Zombies, Skeletons, Goblins, and other foul creatures are here to destroy your home village, but you were already counting on it. Construct Ballistic, Sonic and Death Ray turrets to actively take down your enemies. Wield your trusty Sword and confront their lines head-on. Surround the field with Damage and Speed turrets to increase your damage output.
Your strategy and how you upgrade your weapons will depend on the type of loot dropped by the fallen. Imbued your sword and turrets with the power of elemental gems (Fire, Ice, Plasma) to double your damage against the weakest typing. Passive turrets can also be enhanced with elemental gems to temporarily change your sword’s typing in the heat of battle inside their influence area.
Protect your village. Take them all out.
This game was developed by 2 designers and 2 programmers with the idea to learn more about UE5 and improve our portfolio with another game.
You can find more information about all the projects in his itch.io website and the source code in this link.
In this project I developed a lot of gameplay features and systems. Also I developed some things for the UI of the game.
- Engine : Unreal Engine 5.1
- Languages Used : C++, Blueprints
- Role : Gameplay, AI, System Programmer.
My Contribuctions
In this project we wanted to allow designers to create as many enemies as they wanted so we created a Data Driven Table to create enemies from scratch with the different assets they needed.

With the chosen configuration we created the enemy taken from the enemyPooler. In this game the enemies start appearing depending on the round and the amount and type are selected randomly.
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "TDEnemiesDataTable.h"
#include "TDWeightManager.generated.h"
class ATDEnemy;
/**
*
*/
UCLASS()
class TOWERDEFENSE_API UTDWeightManager : public UObject
{
GENERATED_BODY()
public:
UTDWeightManager();
public:
UPROPERTY(EditAnywhere)
int32 WeightPerRound;
UPROPERTY(EditAnywhere)
int32 ActualRound;
TMap enemiesPerClass;
protected:
private:
UPROPERTY(Transient)
UDataTable* enemiesDatatable;
UPROPERTY()
int32 actualWegith = 0;
UPROPERTY()
TArray actualRoundElements;
public:
UFUNCTION()
void TDSetDataTable(UDataTable* _ref);
///
/// Prepare the round. It select the enemies with the DataTable Values and prepare it for the start of the combat Round.
/// Also clean the previous values for this new round.
///
///
/// Set randomly an element fot every enemy
/// It returns the amount of enemies the player will face to
UFUNCTION()
TArray TDSetActualRound(int32& _atualRound, TArray _roundElement);
//Get a Row from the DataTable with the name of the Row.
UFUNCTION()
void TDGetRowFromDataTable(FName _RowName, FTDEnemiesDataTable& _Row);
//Set the values from the DataTable to the enemy passed
void TDSetEnemyValues(ATDEnemy* _enemyRef, FTDEnemiesDataTable& Row);
protected:
private:
//Get a randomly a Row from the DataTable
FTDEnemiesDataTable* TDSelectRandomRowFromDataTable();
};
#include "GameLogic/TDWeightManager.h"
#include "TDGameData.h"
#include "Map/TDSpawner.h"
#include "Character/TDEnemy.h"
#include "TDObjectPooler.h"
#include "TDEnemiesDataTable.h"
#include "Character/TDEnemyController.h"
#include "AIModule/Classes/BehaviorTree/BehaviorTree.h"
#include "Components/CapsuleComponent.h"
#include "TDElementComponent.h"
#include "TDGameMode.h"
#include "UI/Utilities/TDHealthBar.h"
#include "Components/WidgetComponent.h"
#include "GameplayAbilitySpec.h"
#include "AbilitySystemComponent.h"
UTDWeightManager::UTDWeightManager()
{
actualWegith = 0;
}
TArray UTDWeightManager::TDSetActualRound(int32& _atualRound, TArray _roundElement)
{
actualRoundElements.Empty();
actualRoundElements = _roundElement;
ActualRound = _atualRound;
WeightPerRound = ActualRound * 10;
actualWegith = 0;
enemiesPerClass.Empty();
TArray preparedEnemies;
ATDObjectPooler* objectRef = UTDGameData::TDGetObjectPooler();
ensure(objectRef);
while (!(actualWegith >= WeightPerRound))
{
ATDEnemy* actualEnemy = objectRef->TDGetEnemyFromPool();
if (actualEnemy)
{
FTDEnemiesDataTable* Row = TDSelectRandomRowFromDataTable();
TDSetEnemyValues(actualEnemy, *Row);
preparedEnemies.Add(actualEnemy);
}
}
return preparedEnemies;
}
void UTDWeightManager::TDGetRowFromDataTable(FName _RowName, FTDEnemiesDataTable& _Row)
{
if (enemiesDatatable)
{
FString ContextString = TEXT("Data table context");
TArray RowNames = enemiesDatatable->GetRowNames();
FTDEnemiesDataTable* temp = enemiesDatatable->FindRow(_RowName, ContextString, true);
_Row = *(temp);
}
}
void UTDWeightManager::TDSetDataTable(UDataTable* _ref)
{
enemiesDatatable = _ref;
}
FTDEnemiesDataTable* UTDWeightManager::TDSelectRandomRowFromDataTable()
{
if (enemiesDatatable)
{
FString ContextString = TEXT("Data table context");
FTDEnemiesDataTable* Row = nullptr;
TArray RowNames = enemiesDatatable->GetRowNames();
bool loop = false;
while (!loop)
{
int32 x = FMath::Rand() % RowNames.Num();
FName selectedenemy = RowNames[x];
Row = enemiesDatatable->FindRow(selectedenemy, ContextString, true);
if (Row)
{
//check if the selected enemy's weight is more than the limit weight
loop = Row->weight != -1 && ActualRound >= Row->firstPossibleApperance && WeightPerRound >= actualWegith + Row->weight;
//check if there
if (Row->limitEnemiesPerRound >= 1)
{
if (enemiesPerClass.Contains(x))
{
loop = Row->limitEnemiesPerRound > enemiesPerClass[x];
}
}
if (loop)
{
if (enemiesPerClass.Contains(x))
{
enemiesPerClass[x] = enemiesPerClass[x] + 1;
}
else
{
enemiesPerClass.Add(x, 1);
}
actualWegith += Row->weight;
return Row;
}
}
}
}
return nullptr;
}
void UTDWeightManager::TDSetEnemyValues(ATDEnemy* _enemyRef, FTDEnemiesDataTable& Row)
{
//Debug
_enemyRef->DebugString = Row.DebugName;
//Mesh and anim
_enemyRef->GetMesh()->SetSkeletalMesh(Row.enemyMesh.LoadSynchronous());
if (_enemyRef->DynamicMaterial)
{
_enemyRef->DynamicMaterial->ConditionalBeginDestroy();
}
if (Row.material)
{
_enemyRef->GetMesh()->SetMaterial(0, Row.material);
_enemyRef->DynamicMaterial = _enemyRef->GetMesh()->CreateDynamicMaterialInstance(0, _enemyRef->GetMesh()->GetMaterial(0));
}
else
{
TArray SKMaterials;
SKMaterials = Row.enemyMesh.LoadSynchronous()->GetMaterials();
_enemyRef->GetMesh()->SetMaterial(0, SKMaterials[0].MaterialInterface);
_enemyRef->DynamicMaterial = _enemyRef->GetMesh()->CreateDynamicMaterialInstance(0, _enemyRef->GetMesh()->GetMaterial(0));
}
_enemyRef->GetMesh()->SetRelativeLocation(Row.MeshPosition);
_enemyRef->GetMesh()->SetRelativeScale3D(Row.MeshScale);
_enemyRef->GetMesh()->SetAnimInstanceClass(Row.animationBlueprint);
_enemyRef->TDSetAnimMontaje(Row.animationMontaje.LoadSynchronous());
_enemyRef->GetCapsuleComponent()->SetCapsuleRadius(Row.capsuleRadius);
_enemyRef->GetCapsuleComponent()->SetCapsuleHalfHeight(Row.capsuleHeight);
//GAS
_enemyRef->movementVariation = Row.movementVariation;
_enemyRef->abiliyList = Row.abiliyAsset->abiliyList;
_enemyRef->enemyAttribute = Row.enemyAttribute;
//Weapon
if (!Row.WeaponAssetArray.IsEmpty())
{
int32 x = FMath::Rand() % Row.WeaponAssetArray.Num();
UTDweaponDataAsset* WeaponAsset = Row.WeaponAssetArray[x];
_enemyRef->weaponAssetRef = WeaponAsset;
FAttachmentTransformRules rules = FAttachmentTransformRules(EAttachmentRule::KeepRelative, false);
if (WeaponAsset->assetClass == AssetType::SkeletalMesh)
{
_enemyRef->skeletalWeaponComponent->Activate();
_enemyRef->skeletalWeaponComponent->SetSkeletalMesh(WeaponAsset->skeletalWeaponMesh.LoadSynchronous());
_enemyRef->StaticWeaponComponent->SetStaticMesh(nullptr);
_enemyRef->skeletalWeaponComponent->AttachToComponent(_enemyRef->GetMesh(), rules, WeaponAsset->SocketName);
_enemyRef->StaticWeaponComponent->Deactivate();
}
else if (WeaponAsset->assetClass == AssetType::StaticMesh)
{
_enemyRef->StaticWeaponComponent->Activate();
_enemyRef->StaticWeaponComponent->SetStaticMesh(WeaponAsset->StaticWeaponMesh.LoadSynchronous());
_enemyRef->skeletalWeaponComponent->SetSkeletalMesh(nullptr);
_enemyRef->StaticWeaponComponent->AttachToComponent(_enemyRef->GetMesh(), rules, WeaponAsset->SocketName);
_enemyRef->skeletalWeaponComponent->Deactivate();
}
else
{
_enemyRef->StaticWeaponComponent->SetStaticMesh(nullptr);
_enemyRef->skeletalWeaponComponent->SetSkeletalMesh(nullptr);
_enemyRef->skeletalWeaponComponent->Deactivate();
_enemyRef->StaticWeaponComponent->Deactivate();
}
// if (WeaponAsset->weaponAbility)
// {
// _enemyRef->abilitySystem->GiveAbility(FGameplayAbilitySpec(WeaponAsset->weaponAbility.GetDefaultObject(), 1, 0));
// }
}
else
{
_enemyRef->StaticWeaponComponent->SetStaticMesh(nullptr);
_enemyRef->skeletalWeaponComponent->SetSkeletalMesh(nullptr);
_enemyRef->skeletalWeaponComponent->Deactivate();
_enemyRef->StaticWeaponComponent->Deactivate();
}
//Weight
_enemyRef->unitWeight = Row.weight;
//AI
ATDEnemyController* enemyController = _enemyRef->GetController();
if (enemyController)
{
enemyController->GetBlackboardComponent()->ClearValue("BaseBuild");
enemyController->GetBlackboardComponent()->ClearValue("WaypointPosition");
enemyController->GetBlackboardComponent()->ClearValue("WaypointActor");
enemyController->GetBlackboardComponent()->ClearValue("RealBasePosition");
enemyController->RunBehaviorTree(Row.behaviorTree.LoadSynchronous());
}
//UI
_enemyRef->TDGetHealthBarReference()->TDSetHealthBarSize(Row.HealthBarSize);
_enemyRef->TDGetHealthWidgetComponent()->SetRelativeLocation(Row.HealthBarPosition);
//Element
int y = FMath::Rand() % actualRoundElements.Num();
UTDElementComponent* temp = ITDInterface::Execute_TDGetElementComponent(_enemyRef);
temp->TDSetSpawnedElement(actualRoundElements[y]);
//Loot
UDataTable* lootTable = UTDGameData::TDGetGameMode()->TDGetDataLootFromElement(actualRoundElements[y]);
_enemyRef->TDSetLootDataTable(lootTable);
if (Row.chanceDataTable)
{
_enemyRef->TDSetChanceDataTable(Row.chanceDataTable);
}
}
With the items that we collected from the enemies, now we have to use it in the different structures we have along the map. All of these structures share our WidgetShopComponent.
With this component we lead the logic to open or close the UI to the component and not the structure. In the component you can select which ShopClass you want to select and at the beginning of the game it creates just one instance of each class and shares it with all with the same components that have the same class.
class UTDCostWidget;
class UPrimitiveComponent;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnOpenUISignature, UTDCostWidget*, _widget);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCloseUISignature);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class TOWERDEFENSE_API UTDWidgetShopComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTDWidgetShopComponent();
public:
UPROPERTY(EditAnywhere, Category = "User Interface")
TSubclassOf widgetClass;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "User Interface")
float distanceToUI;
UPROPERTY(VisibleAnywhere, Category = "User Interface")
UTDCostWidget* widgetRef;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "User Interface")
bool isUIActive;
protected:
private:
UPROPERTY()
float distSquared;
UPROPERTY()
FOnOpenUISignature FOnOpenUIDelegate;
UPROPERTY()
FOnCloseUISignature FOnCloseUIDelegate;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void TDHideUI();
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void TDVisibleUI();
UFUNCTION(BlueprintCallable)
void TDOnClickedActor(AActor* Target, FKey ButtonPressed);
protected:
virtual void BeginPlay() override;
private:
UFUNCTION(BlueprintPure)
float TDCheckDistanceWithPlayer();
UFUNCTION(BlueprintPure)
bool TDCheckPlayerInRange();
UFUNCTION(BlueprintPure)
bool TDCanShowUI();
UFUNCTION()
void TDOnCombatPhaseStart(int32 _value);
UFUNCTION()
void TDSetPlayerHUDVisibility(ESlateVisibility _visibility);
UFUNCTION()
void TDOnHoveredActor(AActor* _actor);
UFUNCTION()
void TDOnUnhoveredActor(AActor* _actor);
UFUNCTION()
void TDChangeCursor(EMouseCursor::Type _cursor);
};
UTDWidgetShopComponent::UTDWidgetShopComponent()
{
PrimaryComponentTick.bCanEverTick = true;
isUIActive = false;
distanceToUI = 1000.f;
}
void UTDWidgetShopComponent::BeginPlay()
{
Super::BeginPlay();
distSquared = distanceToUI * distanceToUI;
GetOwner()->OnClicked.AddDynamic(this, &UTDWidgetShopComponent::TDOnClickedActor);
GetOwner()->OnBeginCursorOver.AddDynamic(this, &UTDWidgetShopComponent::TDOnHoveredActor);
GetOwner()->OnEndCursorOver.AddDynamic(this, &UTDWidgetShopComponent::TDOnUnhoveredActor);
UTDGameData::TDGetRoundManager()->FOnCombatPhaseStartDelegate.AddDynamic(this, &UTDWidgetShopComponent::TDOnCombatPhaseStart);
ATDPlayerController* playerController = UTDGameData::TDGetPlayerRef()->GetController();
FOnOpenUIDelegate.AddDynamic(playerController, &ATDPlayerController::TDOnOpenUI);
FOnCloseUIDelegate.AddDynamic(playerController, &ATDPlayerController::TDOnCloseUI);
widgetRef = UTDGameData::TDGetGameMode()->TDGetWidgetFromClass(widgetClass);
}
void UTDWidgetShopComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (isUIActive && !TDCheckPlayerInRange())
{
TDHideUI();
}
}
void UTDWidgetShopComponent::TDHideUI_Implementation()
{
isUIActive = false;
if (widgetRef)
{
widgetRef->SetVisibility(ESlateVisibility::Collapsed);
TDSetPlayerHUDVisibility(ESlateVisibility::HitTestInvisible);
FOnCloseUIDelegate.Broadcast();
}
}
void UTDWidgetShopComponent::TDVisibleUI_Implementation()
{
isUIActive = true;
if (widgetRef)
{
widgetRef->TDSetOwner(this);
widgetRef->SetVisibility(ESlateVisibility::Visible);
widgetRef->TDFadeIn();
TDSetPlayerHUDVisibility(ESlateVisibility::Collapsed);
FOnOpenUIDelegate.Broadcast(widgetRef);
}
}
void UTDWidgetShopComponent::TDOnClickedActor(AActor* Target, FKey ButtonPressed)
{
if (TDCanShowUI())
{
TDVisibleUI();
}
}
float UTDWidgetShopComponent::TDCheckDistanceWithPlayer()
{
float distanceSquared;
FVector ownerLocation = GetOwner()->GetActorLocation();
FVector playerLocation = UTDGameData::TDGetPlayerRef()->GetActorLocation();
distanceSquared = FVector::Dist2D(ownerLocation, playerLocation);
return distanceSquared;
}
bool UTDWidgetShopComponent::TDCheckPlayerInRange()
{
// if (TDCheckDistanceWithPlayer() > distSquared)
// {
// return false;
// }
if (TDCheckDistanceWithPlayer() > distanceToUI)
{
return false;
}
return true;
}
bool UTDWidgetShopComponent::TDCanShowUI()
{
if (!TDCheckPlayerInRange())
{
return false;
}
if (UTDGameData::TDGetRoundManager()->TDGetActualPhase() != GamePhase::BuyPhase)
{
return false;
}
return true;
}
void UTDWidgetShopComponent::TDOnCombatPhaseStart(int32 _value)
{
if (isUIActive)
{
TDHideUI();
}
}
void UTDWidgetShopComponent::TDSetPlayerHUDVisibility(ESlateVisibility _visibility)
{
ATDPlayerController* controller = UTDGameData::TDGetPlayerRef()->GetController();
if (controller)
{
controller->TDGetPLayerHUD()->TDVisibilityToShopUIs(_visibility);
}
}
void UTDWidgetShopComponent::TDOnHoveredActor(AActor* _actor)
{
TDChangeCursor(EMouseCursor::Type::Hand);
}
void UTDWidgetShopComponent::TDOnUnhoveredActor(AActor* _actor)
{
TDChangeCursor(EMouseCursor::Type::Default);
}
void UTDWidgetShopComponent::TDChangeCursor(EMouseCursor::Type _cursor)
{
ATDPlayerController* controller = UTDGameData::TDGetPlayerRef()->GetController();
if (controller)
{
controller->CurrentMouseCursor = _cursor;
}
}


When our hero kill an enemy it drops a loot, we need the blueprints to create and upgrade the differents towers and upgrades for the hero, with all of this we wanted to notify the player which item is he getting from killing the enemy so we created a log at right side of the screen to show the player with item is getting.
We have to create on one hand the logCard, it contains the animation logic and delegates to alert the owner when an animation ends to continue with other logic. We have to set the background color, the amount, and the object type. On the other hand we have to create a widget, it contains all the cards and the logic to show a logCard. When the player receives an item the widget registers it and prepares a logCard and starts its logic, when the logCard ends the FadeIn Animation alerts the widget, so it can prepare a new card. In this class we created a small pool of logCards to avoid the problem of creating and destroying objects.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FActionWidgetAnimationSignature, UTDLogCard*, _card);
/**
*
*/
UCLASS()
class TOWERDEFENSE_API UTDLogCard : public UUserWidget
{
GENERATED_BODY()
public:
public:
UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
UImage* ownerImage;
UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
UBorder* ownerBorder;
UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
UTDText* ownerText;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
FTDItemStruct itemStruct;
UPROPERTY(BlueprintAssignable, BlueprintCallable, BlueprintReadWrite)
FActionWidgetAnimationSignature FStartWidgetAnimationFadeInDelegate;
UPROPERTY(BlueprintAssignable, BlueprintCallable, BlueprintReadWrite)
FActionWidgetAnimationSignature FStartWidgetAnimationFadeOutDelegate;
UPROPERTY(BlueprintAssignable, BlueprintCallable, BlueprintReadWrite)
FActionWidgetAnimationSignature FEndWidgetAnimationFadeInDelegate;
UPROPERTY(BlueprintAssignable, BlueprintCallable, BlueprintReadWrite)
FActionWidgetAnimationSignature FEndWidgetAnimationFadeOutDelegate;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Transient, meta = (BindWidgetAnim))
UWidgetAnimation* FadeIn;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Transient, meta = (BindWidgetAnim))
UWidgetAnimation* FadeOut;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Transient, meta = (BindWidgetAnim))
UWidgetAnimation* UpAnimation;
UPROPERTY(EditAnywhere)
TMap ItemMapImage;
UPROPERTY(EditAnywhere)
TMap ColorBackgroundRarity;
protected:
private:
public:
virtual bool Initialize() override;
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
UFUNCTION(BlueprintImplementableEvent)
void TDPlayVerticalAnimation();
UFUNCTION(BlueprintImplementableEvent)
void TDPlayFadeInAnimation();
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void TDPlayFadeOutAnimation();
UFUNCTION()
void TDPrepareCard(FTDItemStruct _item);
protected:
private:
};
bool UTDLogCard::Initialize()
{
Super::Initialize();
itemStruct = FTDItemStruct();
return true;
}
void UTDLogCard::NativePreConstruct()
{
Super::NativePreConstruct();
if (ItemMapImage.IsEmpty())
{
for (ELootItems item : TEnumRange())
{
if (item != ELootItems::None)
{
ItemMapImage.Add(item);
}
}
}
}
void UTDLogCard::NativeConstruct()
{
Super::NativeConstruct();
}
void UTDLogCard::TDPrepareCard(FTDItemStruct _item)
{
itemStruct = _item;
ownerImage->SetBrushFromTexture(ItemMapImage[_item.dropLoot]);
ownerBorder->SetBrushColor(ColorBackgroundRarity[_item.categoryLoot]);
ownerText->TDSetCustomText(FText::FromString("+" + FString::FromInt(_item.amountLoot)));
}
class UTDLogCard;
/**
*
*/
UCLASS()
class TOWERDEFENSE_API UTDLogWidget : public UTDUserWidget
{
GENERATED_BODY()
public:
virtual bool Initialize() override;
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
public:
UPROPERTY(EditAnywhere)
TSubclassOf cardClass;
UPROPERTY(Transient)
TArray cardsArray;
UPROPERTY(Transient)
TArray disabledCard;
UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
class UCanvasPanel* canvasPanelWidget;
protected:
UPROPERTY()
TArray ItemsHeap;
private:
public:
UFUNCTION()
void TDAddLogItem(ELootItems _item, FGameplayTag _category, int32 amount);
UFUNCTION()
void TDOnEndFadeInAnimation(UTDLogCard* _card);
UFUNCTION()
void TDOnEndFadeOutAnimation(UTDLogCard* _card);
UFUNCTION()
void TDOnStartFadeInAnimation(UTDLogCard* _card);
UFUNCTION()
void TDOnStartFadeOutAnimation(UTDLogCard* _card);
protected:
private:
UFUNCTION()
void TDAddCardToUI();
};
bool UTDLogWidget::Initialize()
{
Super::Initialize();
if (UTDGameData::TDGetGameMode())
{
UTDGameData::TDGetGameMode()->FLootDropDelegate.AddDynamic(this, &UTDLogWidget::TDAddLogItem);
for (int i = 0; i <= 20; ++i)
{
UTDLogCard* cardRef = CreateWidget(canvasPanelWidget, cardClass);
disabledCard.Add(cardRef);
}
}
return true;
}
void UTDLogWidget::NativePreConstruct()
{
Super::NativePreConstruct();
}
void UTDLogWidget::NativeConstruct()
{
Super::NativeConstruct();
}
void UTDLogWidget::TDAddLogItem(ELootItems _item, FGameplayTag _category, int32 amount)
{
FTDItemStruct newItem = FTDItemStruct(_item, _category, amount);
if (!ItemsHeap.IsEmpty())
{
ItemsHeap.Add(newItem);
return;
}
ItemsHeap.Add(newItem);
TDAddCardToUI();
}
void UTDLogWidget::TDAddCardToUI()
{
UTDLogCard* cardRef = disabledCard[0];
disabledCard.RemoveAt(0);
UCanvasPanelSlot* canvasSlotRef = canvasPanelWidget->AddChildToCanvas(cardRef);
for (UTDLogCard* iter : cardsArray)
{
iter->TDPlayVerticalAnimation();
}
cardsArray.Add(cardRef);
cardRef->FEndWidgetAnimationFadeInDelegate.AddDynamic(this, &UTDLogWidget::TDOnEndFadeInAnimation);
cardRef->FEndWidgetAnimationFadeOutDelegate.AddDynamic(this, &UTDLogWidget::TDOnEndFadeOutAnimation);
cardRef->FStartWidgetAnimationFadeInDelegate.AddDynamic(this, &UTDLogWidget::TDOnStartFadeInAnimation);
cardRef->FStartWidgetAnimationFadeOutDelegate.AddDynamic(this, &UTDLogWidget::TDOnStartFadeOutAnimation);
FAnchors anchor = FAnchors(1.f,0.5f);
canvasSlotRef->SetAnchors(anchor);
canvasSlotRef->SetAlignment(FVector2D(1.f,0.5f));
cardRef->SetRenderTranslation(FVector2D(0.f, 100.f));
cardRef->TDPrepareCard(ItemsHeap[0]);
cardRef->TDPlayFadeInAnimation();
}
void UTDLogWidget::TDOnEndFadeInAnimation(UTDLogCard* _card)
{
ItemsHeap.RemoveAt(0);
_card->FEndWidgetAnimationFadeInDelegate.Clear();
if (!ItemsHeap.IsEmpty())
{
TDAddCardToUI();
}
}
void UTDLogWidget::TDOnEndFadeOutAnimation(UTDLogCard* _card)
{
_card->FEndWidgetAnimationFadeInDelegate.Clear();
_card->FEndWidgetAnimationFadeOutDelegate.Clear();
_card->FStartWidgetAnimationFadeInDelegate.Clear();
_card->FStartWidgetAnimationFadeOutDelegate.Clear();
_card->StopAllAnimations();
canvasPanelWidget->RemoveChild(_card);
cardsArray.Remove(_card);
disabledCard.Add(_card);
}
void UTDLogWidget::TDOnStartFadeInAnimation(UTDLogCard* _card)
{
_card->FStartWidgetAnimationFadeInDelegate.Clear();
}
void UTDLogWidget::TDOnStartFadeOutAnimation(UTDLogCard* _card)
{
_card->FStartWidgetAnimationFadeOutDelegate.Clear();
}
Our hero can change the type of element that contains his sword to deal more damage to enemies that are weak to this type of element. First we have to create all the elements in an enumerator and a DataAsset that contains the element and the multipliers it does to others elements.
UENUM(BlueprintType)
enum class EElements : uint8
{
None = 0,
Fire = 1,
Freeze = 2,
Plasma = 3
};
UCLASS()
class TOWERDEFENSE_API UTDElement : public UDataAsset
{
GENERATED_BODY()
public:
UTDElement();
public:
UPROPERTY(EditDefaultsOnly)
EElements ownerElement = EElements::None;
UPROPERTY(EditDefaultsOnly)
TMap ElementsMap;
protected:
private:
public:
protected:
private:
};
With all of this we can create a component for all the characters of the game. In this component we can select the spawn element or a temporal element if the enemy or the player enters an area that changes the element while they are in the area.
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class TOWERDEFENSE_API UTDElementComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTDElementComponent();
public:
UPROPERTY(BlueprintAssignable)
FOnElementChangeSignature OnElementChangeDelegate;
protected:
private:
UPROPERTY(VisibleAnywhere)
EElements ownerElement = EElements::None;
//DataAsset that store the ownerElement and the damage multipliers to other classes
UPROPERTY(VisibleAnywhere)
UTDElement* SpawnedElementData;
UPROPERTY(VisibleAnywhere)
UTDElement* TemporalElementData;
TMap heapTemporalElements;
public:
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
UFUNCTION(BlueprintCallable)
void TDSetSpawnedElement(EElements _element);
UFUNCTION(BlueprintCallable)
void TDSetTemporalElement(AActor* _instigator,EElements _element);
UFUNCTION(BlueprintCallable)
void TDRemoveTemporalElement(AActor* _instigator);
UFUNCTION(BlueprintPure)
EElements TDGetOwnerElement();
UFUNCTION(BlueprintCallable)
UTDElement* TDGetActualDataAsset();
UFUNCTION(BlueprintPure)
float TDGetDamageMultiplier(EElements _element);
UFUNCTION(BlueprintPure)
EElements TDGetSpawnedElement();
protected:
// Called when the game starts
virtual void BeginPlay() override;
private:
};
UTDElementComponent::UTDElementComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UTDElementComponent::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void UTDElementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
}
void UTDElementComponent::TDSetSpawnedElement(EElements _element)
{
SpawnedElementData = UTDGameData::TDGetGameMode()->TDGetDataAssetFromElement(_element);
ownerElement = SpawnedElementData->ownerElement;
OnElementChangeDelegate.Broadcast(ownerElement);
}
void UTDElementComponent::TDSetTemporalElement(AActor* _instigator,EElements _element)
{
TemporalElementData = UTDGameData::TDGetGameMode()->TDGetDataAssetFromElement(_element);
ownerElement = TemporalElementData->ownerElement;
heapTemporalElements.Add(_instigator, _element);
OnElementChangeDelegate.Broadcast(ownerElement);
}
void UTDElementComponent::TDRemoveTemporalElement(AActor* _instigator)
{
heapTemporalElements.Remove(_instigator);
if (heapTemporalElements.IsEmpty())
{
TemporalElementData = nullptr;
ownerElement = SpawnedElementData->ownerElement;
OnElementChangeDelegate.Broadcast(ownerElement);
}
else
{
TArray keys;
heapTemporalElements.GetKeys(keys);
EElements NextHeapElement = heapTemporalElements[keys[0]];
TemporalElementData = UTDGameData::TDGetGameMode()->TDGetDataAssetFromElement(NextHeapElement);
ownerElement = TemporalElementData->ownerElement;
OnElementChangeDelegate.Broadcast(ownerElement);
}
}
EElements UTDElementComponent::TDGetOwnerElement()
{
return ownerElement;
}
UTDElement* UTDElementComponent::TDGetActualDataAsset()
{
if (TemporalElementData)
{
return TemporalElementData;
}
return SpawnedElementData;
}
float UTDElementComponent::TDGetDamageMultiplier(EElements _element)
{
UTDElement* tempRef = SpawnedElementData;
if (TemporalElementData)
{
tempRef = TemporalElementData;
}
return tempRef->ElementsMap[_element];
}
EElements UTDElementComponent::TDGetSpawnedElement()
{
return SpawnedElementData->ownerElement;
}
Another important thing we did at the beginning of the project was an enemyPooler. In this kind of game that we are destroying and creating enemies all the time we wanted to have a pool to avoid this problem.
class ATDEnemy;
UCLASS()
class TOWERDEFENSE_API ATDObjectPooler : public AActor
{
GENERATED_BODY()
public:
ATDObjectPooler();
~ATDObjectPooler();
public:
UPROPERTY(EditAnywhere)
TArray> enemiesClasses;
UPROPERTY(EditAnywhere)
int32 InitialSpawn = 5;
protected:
private:
UPROPERTY(Transient)
TArray disabledEnemies;
UPROPERTY(Transient)
TArray activeEnemies;
public:
ATDEnemy* TDGetEnemyFromPool();
void TDAddEnemyToPool(ATDEnemy* _enemyRef);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
// Called every frame
virtual void Tick(float DeltaTime) override;
private:
};
ATDEnemy* ATDObjectPooler::TDGetEnemyFromPool()
{
if (!disabledEnemies.IsEmpty())
{
ATDEnemy* enemyRef = disabledEnemies[0];
disabledEnemies.RemoveAt(0);
activeEnemies.Add(enemyRef);
return enemyRef;
}
else
{
UWorld* actualWorld = UTDGameData::TDGetWorld();
ATDEnemy* enemyRef = actualWorld->SpawnActor(enemiesClasses[0]);
enemyRef->TDSetDisable();
activeEnemies.Add(enemyRef);
return enemyRef;
}
return nullptr;
}
void ATDObjectPooler::TDAddEnemyToPool(ATDEnemy* _enemyRef)
{
activeEnemies.Remove(_enemyRef);
disabledEnemies.Add(_enemyRef);
}
// Sets default values
ATDObjectPooler::ATDObjectPooler()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
ATDObjectPooler::~ATDObjectPooler()
{
}
// Called when the game starts or when spawned
void ATDObjectPooler::BeginPlay()
{
Super::BeginPlay();
UWorld* actualWorld = UTDGameData::TDGetWorld();
for (int i = 0; i < InitialSpawn; ++i)
{
ATDEnemy* enemyRef = actualWorld->SpawnActor(enemiesClasses[0]);
enemyRef->TDSetDisable();
disabledEnemies.Add(enemyRef);
}
}
// Called every frame
void ATDObjectPooler::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}