Horde Havoc

Horde Havoc

Project Details


Production Time - 4 weeks, full time

Team Size - 12

Role - Lead programmer

Responsibilities - Gameplay programming, UI/UX, UI Animation

Engine - Unreal Engine

Elemental Components


In Horde Havoc the central game mechanic was for different effects to be applicable to orcs, and for that effect to spread to nearby orcs, aswell as effect the environment. To achieve this me and one other programmer started out working on components to represent these different effects (fire, oil, etc.).


Once we had those components we created a system with the job of checking the proximity of those components as well as keeping track of things like how long they had been active or how long they had been nearby something that affects them. 


This system was based around an octtree, a spatial data structure, which allowed us to quickly check the proximity of components without sacrificing too much performance.

Game Description


Horde Havoic is a casual puzzle game with real time strategy-esque controls, the objective is to lead a horde of orcs and to use them to reach your goals. The orcs are both controllable characters and a resource for the player to spend to reach the end of the level. 


The game is based around a system where orcs are affected by their environment and their surrounding orc buddies, for instance if one orc walks into a flame and catches fire, that fire can spread to nearby orcs and if you're not careful the situtation can quickly spiral out of control.




#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GenericOctree.h"
#include "BSElementComponentSystem.generated.h"


//Octtree
USTRUCT()
struct FOctTreeElement
{
	GENERATED_BODY()
	int32 Id;
	FBoxSphereBounds BoxSphereBounds;
	AActor* MyActor;
};
struct FOctTreeSemantics
{
	enum { MaxElementsPerLeaf = 2 };
	enum { MinInclusiveElementsPerNode = 7 };
	enum { MaxNodeDepth = 12 };

	typedef TInlineAllocator<MaxElementsPerLeaf> ElementAllocator;

	FORCEINLINE static FBoxSphereBounds GetBoundingBox(const FOctTreeElement& Element)
	{
		return Element.BoxSphereBounds;
	}
	FORCEINLINE static bool AreElementsEqual(const FOctTreeElement& A, const FOctTreeElement& B)
	{
		return A.Id == B.Id;
	}
	FORCEINLINE static void SetElementId(const FOctTreeElement& Element, FOctreeElementId Id) { }
	FORCEINLINE static void ApplyOffset(FOctTreeElement& Element, FVector Offset)
	{
		FVector NewPostion = Element.MyActor->GetActorLocation() + Offset;
		Element.MyActor->SetActorLocation(NewPostion);
		Element.BoxSphereBounds.Origin = NewPostion;
	}
};
typedef TOctree<FOctTreeElement, FOctTreeSemantics> FOrcTree;

UCLASS()
class TEAM2_PROJECT2_API ABSElementComponentSystem : public AActor
{
	GENERATED_BODY()

public:	

	UPROPERTY(EditAnywhere)
	float Extent = 12000.f;
	UPROPERTY(EditDefaultsOnly)
	FRuntimeFloatCurve Curve;
	UPROPERTY(EditAnywhere)
	bool DebugDrawGetAtLocation = false;
	UPROPERTY(EditDefaultsOnly)
	bool DebugDrawEntireOrcTree = false;

private:

	int32 uidCounter = 0;
	FOrcTree* OrcTree = nullptr;
	TArray<int32> ComponentsToDestroy;
	TArray<FOctTreeElement*> elements;
	TMap<int32, class UBSElementComponentBase*> AllComponents;
	//Burnables
	TMap<int32, class UBSBurnableComponent*> Burnables;
	TArray<int32> BurnableComponents;
	TArray<int32> CurrentlyIgniting;
	TMap<int32, float> IgnitionTimers;
	TMap<int32, float> DeathTimers;
	TSet<int32> InfiniteFires;
	//Explosives
	TMap<int32, class UBSExplosiveComponent*> Explosives;
	TArray<int32> ExplosiveComponents;
	TMap<int32, float> PrimeTimers;
	TMap<int32, FVector> LocationsOfLastTrail;
	TMap<int32, int32> OrcsWalkedInto;
	TSet<int32> CheckedComponents;
	//Explodables
	TMap<int32, class UBSExplodableComponent*> Explodables;
	TArray<int32> ExplodableComponents;
	//Ice
	TMap<int32, class UBSIceComponent*> Freezables;
	TArray<int32> IceComponents;
	TArray<int32> CurrentlyFreezing;
	TMap<int32, float> FreezeTimers;


	//Constants
	const float ROOT_3 = 1.73205080757f;
	const float SPHERE_TO_BOX_FACTOR = 1.5f;
	const float MINIMUM_RADIUS = 25.f;

protected:
	virtual void BeginPlay() override;

public:	
	ABSElementComponentSystem();
	virtual void Tick(float DeltaTime) override;
	void AddElementComponent(class UBSElementComponentBase* component);
	void AddToComponentToDestroy(int32 id);
private:
	void RemoveElementComponent(int32 id);
	//Octree
	void UpdateTree();
	void DrawOrcTree();
	//functionality using octree
	UFUNCTION()
	void UpdateSpreading();
	void UpdateBurnables(float DeltaTime);
	void UpateExplosives(float DeltaTime);
	void UpdateFreezeables(float DeltaTime);
	void GetElementsAtPosition(const FVector& center, float extent, TArray<int32>& found) const;
	FBoxSphereBounds GetBoxSphereFromRadius(const FVector& center, float radius) const;
};


    



void ABSElementComponentSystem::UpdateTree()
{
	if (OrcTree != nullptr) 
		OrcTree->Destroy();

	const FVector min = FVector::OneVector * -Extent;
	const FVector max = FVector::OneVector * Extent;
	OrcTree = new FOrcTree(GetActorLocation(), FBox(min, max).GetExtent().GetMax());

	for (TPair<int32, UBSElementComponentBase*> pair : AllComponents)
	{
		if (!IsValid(pair.Value) || !IsValid(pair.Value->GetOwner()))
		{
			ComponentsToDestroy.Add(pair.Key);
			continue;
		}

		const int32 id = pair.Key;;
		FOctTreeElement element;
		element.Id = id;
		element.MyActor = AllComponents[id]->GetOwner();
		FVector origin;
		FVector extent;
		AllComponents[id]->GetOwner()->GetActorBounds(true, origin, extent);
		const float radius = FMath::Max(MINIMUM_RADIUS, FMath::Min(extent.X, extent.Y));
		origin = AllComponents[id]->GetOwner()->GetActorLocation();
		const FBoxSphereBounds boxSphereBounds = GetBoxSphereFromRadius(origin, radius);
		element.BoxSphereBounds = boxSphereBounds;
		OrcTree->AddElement(element);
	}
	for (int32& id : ComponentsToDestroy)
	{
		RemoveElementComponent(id);
	}
	ComponentsToDestroy.Empty();
}
void ABSElementComponentSystem::UpdateSpreading()
{
	CurrentlyIgniting.Empty();
	CurrentlyFreezing.Empty();
	CheckedComponents.Empty();
	TArray<int32> foundElements;
	for (int32& id : BurnableComponents)
	{
		//If burning, check for nearby burnables and ignite them
		if (Burnables[id]->IsBurning)
		{
			UBSBurnableComponent* burnable = Burnables[id];
			GetElementsAtPosition(burnable->GetOwner()->GetActorLocation(), burnable->FireSpreadRadius, foundElements);
			for (int32 id2 : foundElements)
			{
				if (CheckedComponents.Contains(id2))
					continue;
				
				if (BurnableComponents.Contains(id2) && !Burnables[id2]->IsBurning)
				{
					CurrentlyIgniting.Add(id2);
					CheckedComponents.Add(id2);
				}
				if (ExplosiveComponents.Contains(id2) && Explosives[id2]->IsActivated)
				{
					if(Explosives[id2]->OnFuseIgnition.IsBound())
						Explosives[id2]->OnFuseIgnition.Broadcast();
					Explosives[id2]->IsPrimed = true;
				}
			}
		}
	}
	for (int32& id : IceComponents)
	{
		if (Freezables[id]->CanFreezeOthers)
		{
			GetElementsAtPosition(Freezables[id]->GetOwner()->GetActorLocation(), Freezables[id]->IceSpreadRadius, foundElements);
			for (int32& id2 : foundElements)
			{
				if (IceComponents.Contains(id2) && !Freezables[id2]->IsFrozen)
					CurrentlyFreezing.Add(id2);
			}
		}
	}
	for (int32& id : ExplosiveComponents)
	{
		if (Explosives[id]->CanSpreadOil)
		{
			if (OrcsWalkedInto.Contains(id) && OrcsWalkedInto[id] >= Explosives[id]->NumberOfSpreads)
			{
				Explosives[id]->CanSpreadOil = false;
				break;
			}
			
			GetElementsAtPosition(Explosives[id]->GetOwner()->GetActorLocation(), Explosives[id]->OilSpreadRadius, foundElements);
			for (int32 id2 : foundElements)
			{
				if (BurnableComponents.Contains(id2) && Burnables[id2]->IsBurning)
					continue;
				if (ExplosiveComponents.Contains(id2) && !Explosives[id2]->IsActivated)
				{	
					Explosives[id2]->IsActivated = true;
					if (OrcsWalkedInto.Contains(id))
						OrcsWalkedInto[id]++;
					else
						OrcsWalkedInto.Add(id, 1);
					if (Explosives[id]->OnOilSpread.IsBound())
						Explosives[id]->OnOilSpread.Broadcast(OrcsWalkedInto[id]);
					
					if (Explosives[id2]->OnActivated.IsBound())
						Explosives[id2]->OnActivated.Broadcast();
				}
			}
		}
	}
}
void ABSElementComponentSystem::GetElementsAtPosition(const FVector& center, float radius, TArray<int32>& found) const
{
	const FBoxSphereBounds boxSphereBounds = GetBoxSphereFromRadius(center, radius);
	const FBoxCenterAndExtent inBoundingBoxQuery = FBoxCenterAndExtent(boxSphereBounds);
	if (DebugDrawGetAtLocation)
	{
		DrawDebugSphere(GetWorld(), center, boxSphereBounds.SphereRadius, 12, FColor::Red, false, 0, 0, 1);
		DrawDebugBox(GetWorld(), center, boxSphereBounds.BoxExtent, FColor::Red, false, 0, 0, 3);
	}
	found.Empty();
	for (FOrcTree::TConstElementBoxIterator<> OctreeIt(*OrcTree, inBoundingBoxQuery); OctreeIt.HasPendingElements(); OctreeIt.Advance())
		found.Add(OctreeIt.GetCurrentElement().Id);
}
FBoxSphereBounds ABSElementComponentSystem::GetBoxSphereFromRadius(const FVector& center, float radius) const
{
	const float sideLength = (2 * radius) / ROOT_3;
	const FVector extent = FVector(sideLength, sideLength, sideLength) * SPHERE_TO_BOX_FACTOR * 0.5f;
	return FBoxSphereBounds(center, extent, radius);
}

    

UI Animations


I was also responsible for some of the UI animations in the game. I worked closely with the 2D artists and implemented their designs into the engine. Since our 2D artists did not have the time to animate all the UI elements in game I got a specification from our art lead and implemented the animations in the engine.

Misc


I also contributed with a few other things; like adding some randomization in the appearance of the orcs, their skin color, different

accessories (weapons and helmets) as well as what happend when they took damage and died (spawning particles, sound, ragdolling, etc)


I also wrote the camera controller as well as  the way the player interacts with the world through clicking, box selection, etc.



#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Components/BSElementComponentSystem.h"
#include "BSRTSCamera.generated.h"

UCLASS()
class TEAM2_PROJECT2_API ABSRTSCamera : public APawn
{
	GENERATED_BODY()

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Components)
	class USceneComponent* CameraAttachmentPoint;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Components)
	class USpringArmComponent* CameraArm;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Components)
	class UCameraComponent* Camera;

	UPROPERTY(EditDefaultsOnly, Category = Movement)
	float HeightBuffer = 15.0f;
	UPROPERTY(EditDefaultsOnly, Category = Movement)
	float WidthBuffer = 15.0f;
	UPROPERTY(EditDefaultsOnly, Category = Movement)
	float Speed = 1000;
	UPROPERTY(EditDefaultsOnly, Category = Movement)
	float Acceleration = 100;
	UPROPERTY(EditDefaultsOnly, Category = Movement)
	float Deceleration = 100;
	UPROPERTY(EditDefaultsOnly, Category = Movement)
	float FollowCameraSmoothing = 1000;
	UPROPERTY(EditDefaultsOnly, Category = Movement)
	float RotationSpeed = 1000;
	UPROPERTY(EditDefaultsOnly, Category = Zoom)
	float ZoomSpeed = 1000;
	UPROPERTY(EditDefaultsOnly, Category = Zoom)
	float MinDistance = 300.0f;
	UPROPERTY(EditDefaultsOnly, Category = Zoom)
	float MaxDistance = 1000.0f;
	UPROPERTY(EditDefaultsOnly, Category = Zoom)
	float ClampAngle = 35.0f;
	UPROPERTY(EditDefaultsOnly)
	float LerpSpeed = 40.f;
	UPROPERTY(EditDefaultsOnly, Category = Bounding)
	FString BoundingBoxName = "BoundingBox";

	UPROPERTY(EditDefaultsOnly, Category = CameraShake)
	TSubclassOf<UCameraShake> ExplosionShake;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	bool Activated = true;

private:
	class ABSPlayerController* PlayerController;
	float RotationInput;
	float HorizontalInput;
	float VerticalInput;
	float ZoomInput;
	float CurrentSpeed;
	FVector MovementDirection;
	float TargetHeight;
	class ATriggerBox* BoundingBox = nullptr;
	bool FocusOnUnits;
	const float MINIMUM_ACCEPTED_INPUT = 0.1f;
	FRotator StartRotation;
public:
	ABSRTSCamera();
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;
	virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
	void ExplodeShake();
private:
	void UpdateRotation(const float DeltaTime);
	void UpdateZoom(const float DeltaTime);
	void UpdateMovement(const float DeltaTime);
	void FollowUnits(const float DeltaTime);
	void SetRotationInput(float rotation);
	void SetHorizontalInput(float horizontal);
	void SetVerticalInput(float vertical);
	void SetZoomInput(float zoom);
	void FollowGroundLevel(float DeltaTime);
	
	UFUNCTION()
	void SetFocusInput();

};

    



#include "BSRTSCamera.h"
#include <Kismet/GameplayStatics.h>
#include <GameFramework/SpringArmComponent.h>
#include <Camera/CameraComponent.h>
#include <Components/SceneComponent.h>
#include "Engine/TriggerBox.h"
#include <EngineUtils.h>
#include "Core/BSPlayerController.h"
#include "Components/BSExplodableComponent.h"
#include "Components/BSExplosiveComponent.h"
#include "DrawDebugHelpers.h"

ABSRTSCamera::ABSRTSCamera()
{
	PrimaryActorTick.bCanEverTick = true;
	CameraAttachmentPoint = CreateDefaultSubobject<USceneComponent>(TEXT("RootPoint"));
	CameraArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraArm"));
	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));

	RootComponent = CameraAttachmentPoint;
	CameraArm->SetupAttachment(CameraAttachmentPoint);
	Camera->SetupAttachment(CameraArm);
}
void ABSRTSCamera::BeginPlay()
{
	Super::BeginPlay();
	PlayerController = Cast<ABSPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
	PlayerController->OnSelectionUpdate.AddDynamic(this, &ABSRTSCamera::SetFocusInput);
	for (TActorIterator<ATriggerBox> it(GetWorld()); it; ++it)
	{
		if (it->GetName().Equals(BoundingBoxName))
			BoundingBox = *it;
	}
	TargetHeight = GetActorLocation().Z;
	StartRotation = GetActorRotation();
}
void ABSRTSCamera::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (!Activated) return;
	UpdateRotation(DeltaTime);
	UpdateZoom(DeltaTime);
	UpdateMovement(DeltaTime);
	FollowGroundLevel(DeltaTime);

}
void ABSRTSCamera::SetupPlayerInputComponent(UInputComponent* InputComponent)
{
	Super::SetupPlayerInputComponent(InputComponent);
	InputComponent->BindAxis("Rotate", this, &ABSRTSCamera::SetRotationInput);
	InputComponent->BindAxis("Horizontal", this, &ABSRTSCamera::SetHorizontalInput);
	InputComponent->BindAxis("Vertical", this, &ABSRTSCamera::SetVerticalInput);
	InputComponent->BindAxis("Zoom", this, &ABSRTSCamera::SetZoomInput);
	InputComponent->BindAction("Focus", IE_Pressed, this, &ABSRTSCamera::SetFocusInput);
}
void ABSRTSCamera::UpdateRotation(const float DeltaTime)
{
	const FRotator rotator = FRotator(0, RotationInput * RotationSpeed * DeltaTime, 0);

	if (GetActorRotation().Yaw + rotator.Yaw < StartRotation.Yaw + ClampAngle && GetActorRotation().Yaw + rotator.Yaw > StartRotation.Yaw + -ClampAngle)
	{
		AddActorLocalRotation(rotator);
	}
}

void ABSRTSCamera::UpdateZoom(const float DeltaTime)
{
	const float zoom = ZoomInput * ZoomSpeed * DeltaTime;
	float currentLength = CameraArm->TargetArmLength;
	currentLength += zoom;
	currentLength = FMath::Clamp(currentLength, MinDistance, MaxDistance);
	CameraArm->TargetArmLength = currentLength;
}
void ABSRTSCamera::UpdateMovement(const float DeltaTime)
{
	//Check input
	float mouseX = 0.0f, mouseY = 0.0f;
	int32 viewportSizeX = 0, viewportSizeY = 0;
	PlayerController->GetMousePosition(mouseX, mouseY);
	PlayerController->GetViewportSize(viewportSizeX, viewportSizeY);

	const float deltaX = viewportSizeX - mouseX;
	const float deltaY = viewportSizeY - mouseY;

	int horizontal = 0, vertical = 0;

	if (deltaX < WidthBuffer)
		horizontal = 1;
	else if (deltaX > viewportSizeX - WidthBuffer)
		horizontal = -1;

	if (deltaY < HeightBuffer)
		vertical = -1;
	else if (deltaY > viewportSizeY - HeightBuffer)
		vertical = 1;
	if (FMath::Sign(HorizontalInput) != 0) horizontal = HorizontalInput;
	if (FMath::Sign(VerticalInput) != 0) vertical = VerticalInput;
	horizontal = FMath::Clamp(horizontal, -1, 1);
	vertical = FMath::Clamp(vertical, -1, 1);

	//Check if giving input
	const FVector input = FVector(vertical, horizontal, 0.0f);
	const bool givingInput = (input.SizeSquared() > MINIMUM_ACCEPTED_INPUT * MINIMUM_ACCEPTED_INPUT);
	if (givingInput)
	{
		FocusOnUnits = false;
		MovementDirection = input.GetSafeNormal();
	}
	else if (FocusOnUnits)
	{
		FollowUnits(DeltaTime);
	}

	//Apply movement
	CurrentSpeed += (givingInput ? Acceleration : -Deceleration) * DeltaTime;
	CurrentSpeed = FMath::Clamp(CurrentSpeed, 0.0f, Speed);
	AddActorLocalOffset(MovementDirection * CurrentSpeed * DeltaTime);
	//Bounding box
	if (BoundingBox == nullptr)
		return;
	FVector center, bounds;
	BoundingBox->GetActorBounds(false, center, bounds);
	FVector location = GetActorLocation();
	//TODO: rotate bounds x and y
	location.X = FMath::Clamp(location.X, center.X - bounds.X, center.X + bounds.X);
	location.Y = FMath::Clamp(location.Y, center.Y - bounds.Y, center.Y + bounds.Y);
	location.Z = GetActorLocation().Z;
	SetActorLocation(location);
}
void ABSRTSCamera::FollowUnits(const float DeltaTime)
{
	FVector centerPoint;
	bool unitsSelected = PlayerController->GetSelectedCenter(centerPoint);
	centerPoint.Z = GetActorLocation().Z;
	if (unitsSelected)
	{
		const FVector toTarget = centerPoint - GetActorLocation();
		FVector newLocation = GetActorLocation() + toTarget * FMath::Clamp(FollowCameraSmoothing * DeltaTime, 0.0f, 1.0f);
		SetActorLocation(newLocation);
	}
	else
		FocusOnUnits = false;
	
}

void ABSRTSCamera::SetRotationInput(float rotation)
{
	RotationInput = rotation;
}
void ABSRTSCamera::SetHorizontalInput(float horizontal)
{
	HorizontalInput = horizontal;
}
void ABSRTSCamera::SetVerticalInput(float vertical)
{
	VerticalInput = vertical;
}
void ABSRTSCamera::SetZoomInput(float zoom)
{
	ZoomInput = zoom;
}

void ABSRTSCamera::FollowGroundLevel(float DeltaTime)
{
	const FVector startLocation = GetActorLocation() + FVector(0, 0, 2000.f);
	const FVector endLocation = startLocation - FVector(0, 0, 4000.f);
	FHitResult hit;
	GetWorld()->LineTraceSingleByChannel(hit, startLocation, endLocation, ECC_Visibility);
	if (hit.bBlockingHit)
	{
		TargetHeight = hit.Location.Z + 50.f;
	}
	FVector currentLocation = GetActorLocation();
	currentLocation.Z = FMath::Lerp(GetActorLocation().Z, TargetHeight, LerpSpeed * DeltaTime);
	SetActorLocation(currentLocation);
}

void ABSRTSCamera::ExplodeShake()
{
	//GetWorld()->GetFirstPlayerController()->PlayerCameraManager->PlayCameraShake(ExplosionShake, 1.0f);
}

void ABSRTSCamera::SetFocusInput()
{
	FocusOnUnits = true;
}