Horde Havoc

Horde Havoc is a casual puzzler with real-time strategy mechanics where you sacrifice a horde of willing Orcs to solve problems your way.


The player guides their orcs through small bite-sized levels filled with obstacles. Orcs acts as both the player character and a resource to manage. The humorous tone and stylized visuals encourage the player to sacrifice the orcs.

Plattform

PC


Genre

RTS/Puzzle


Team Size

12


Engine

Unreal Engine 4


Language

C++


Duration

4 Weeks

 
    void ABSElementComponentSystem::Tick(float DeltaTime)
#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::UpdateBurnables(float DeltaTime) {
	for (int32& id : BurnableComponents)
	{
		//just started ignition
		if (CurrentlyIgniting.Contains(id) && !IgnitionTimers.Contains(id))
		{
			IgnitionTimers.Add(id, 0.0f);
			if (Burnables[id]->OnStartedIgnition.IsBound())
				Burnables[id]->OnStartedIgnition.Broadcast();
		}
		//just stopped ignition
		if (!CurrentlyIgniting.Contains(id) && IgnitionTimers.Contains(id))
		{
			IgnitionTimers.Remove(id);
			if (Burnables[id]->OnStoppedIgnition.IsBound())
				Burnables[id]->OnStoppedIgnition.Broadcast();
		}
		//Tick ignition
		if (IgnitionTimers.Contains(id))
		{
			IgnitionTimers[id] += DeltaTime;
			if (IgnitionTimers[id] > Burnables[id]->TimeUntilBurning)
			{
				Burnables[id]->IsBurning = true;
				DeathTimers.Add(id);
				IgnitionTimers.Remove(id);
				if (Burnables[id]->OnStartedBurning.IsBound())
					Burnables[id]->OnStartedBurning.Broadcast();
			}
		}
		//If something started burning (i.e. from blueprint)
		if (Burnables[id]->IsBurning && !DeathTimers.Contains(id) && !InfiniteFires.Contains(id))
		{
			if (Burnables[id]->OnStartedBurning.IsBound())
				Burnables[id]->OnStartedBurning.Broadcast();
			if(IgnitionTimers.Contains(id)) IgnitionTimers.Remove(id);
			if (Burnables[id]->TimeUntilDeath == 0.0f)
				InfiniteFires.Add(id);
			else
				DeathTimers.Add(id);
		}
		//If something stopped burning (i.e from water)
		if (!Burnables[id]->IsBurning && DeathTimers.Contains(id))
		{
			DeathTimers.Remove(id);
			if (Burnables[id]->OnStoppedBurning.IsBound())
				Burnables[id]->OnStoppedBurning.Broadcast();
		}

		//Tick death
		if (DeathTimers.Contains(id))
		{
			DeathTimers[id] += DeltaTime;
			if (DeathTimers[id] > Burnables[id]->TimeUntilDeath)
			{
				ComponentsToDestroy.Add(id);
				if (Burnables[id]->OnBurnedDown.IsBound())
					Burnables[id]->OnBurnedDown.Broadcast();
			}
		}		
	}
}
	
void ABSElementComponentSystem::UpateExplosives(float DeltaTime)
{
	TArray< int32> Exploding;

	for (int32& id : ExplosiveComponents)
	{
		if (!Explosives[id]->IsActivated)
			continue;

		//Check if explosive should be leaving trail
		if (Explosives[id]->LeavesBurnableTrail && !LocationsOfLastTrail.Contains(id))
		{
			LocationsOfLastTrail.Add(id, Explosives[id]->GetOwner()->GetActorLocation());
		}
		//Check if a trail bit should be dropped
		if (LocationsOfLastTrail.Contains(id))
		{
			const FVector location = Explosives[id]->GetOwner()->GetActorLocation();
			const float distance = Explosives[id]->FirstTime ? Explosives[id]->FirstDistance : Explosives[id]->DistanceBetweenTrailParts;
			if ((location - LocationsOfLastTrail[id]).SizeSquared() > (distance * distance))
			{
				LocationsOfLastTrail[id] = location;
				Explosives[id]->FirstTime = false;
				if (Explosives[id]->OnTrailUpdate.IsBound())
					Explosives[id]->OnTrailUpdate.Broadcast(location);
			}
		}
		//Check if explosive should be lit
		if (Explosives[id]->IsPrimed)
		{
			if (!PrimeTimers.Contains(id))
			{
				PrimeTimers.Add(id, 0.0f);
				if (Explosives[id]->OnFuseIgnition.IsBound())
					Explosives[id]->OnFuseIgnition.Broadcast();
			}
		}
		//Check if explosive should explode
		if (PrimeTimers.Contains(id))
		{
			PrimeTimers[id] += DeltaTime;
			if (PrimeTimers[id] >= Explosives[id]->FuseTime)
				Exploding.Add(id);
		}
	}
	//Explode components
	TArray< int32> nearbyExplosives;
	TArray< int32> foundExplodables;
	TSet< int32> blast;
	for (int32& id : Exploding)
	{
		if(ComponentsToDestroy.Contains(id))
			continue;
		UBSExplosiveComponent* explosive = Explosives[id];
		bool foundExplosives = false;
		FVector epicenter = explosive->GetOwner()->GetActorLocation();
		float radius = explosive->BlastRadius;
		float power = explosive->ExplosivePower;
		int numberOfExplosives = 1;
		blast.Add(id);
		ComponentsToDestroy.Add(id);
		do
		{
			foundExplosives = false;
			GetElementsAtPosition(epicenter / numberOfExplosives, (radius/numberOfExplosives * Curve.GetRichCurve()->Eval(numberOfExplosives)), nearbyExplosives);
			for (int32& id2 : nearbyExplosives)
			{
				if (ExplosiveComponents.Contains(id2) && !blast.Contains(id2) && Explosives[id2]->IsActivated)
				{
					blast.Add(id2);
					epicenter += Explosives[id2]->GetOwner()->GetActorLocation();
					radius += Explosives[id2]->BlastRadius;
					power += Explosives[id2]->ExplosivePower;
					numberOfExplosives++;
					ComponentsToDestroy.Add(id2);
					foundExplosives = true;
				}
			}
		} while (foundExplosives);
		blast.Empty();

		if (Explosives[id]->OnExplosion.IsBound())
		{
			epicenter /= numberOfExplosives;
			radius = (radius / numberOfExplosives * Curve.GetRichCurve()->Eval(numberOfExplosives));
			
			GetElementsAtPosition(epicenter, radius, foundExplodables);
			for (int32& id2 : foundExplodables)
			{
				if (!Explodables.Contains(id2))
					continue;
				const FVector direction = (Explodables[id2]->GetOwner()->GetActorLocation() - epicenter).GetSafeNormal();
				const float minimumValue = power * Explosives[id]->MinimumExplosiveFactor;
				const float lerpValue = (Explodables[id2]->GetOwner()->GetActorLocation() - epicenter).Size() / radius;
				float actualPower = FMath::Lerp(power, minimumValue, FMath::Clamp(lerpValue, 0.f, 1.f));
				if(Explodables[id2]->OnExploded.IsBound())
					Explodables[id2]->OnExploded.Broadcast(actualPower, direction);
			}
			Explosives[id]->OnExplosion.Broadcast(epicenter, radius, power);
		}
	}
}
    
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);
}
        
    
void ABSElementComponentSystem::UpdateTree()
{
    /* This is done to update positions in the tree
       we should have changed it to not destroy and 
       create a new tree every frame */
	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();
}
       
   
       
       // This is the first iteration we had on the system.
       // That we later changed to use octree instead.
       
void AElementComponentSystem::UpdateBurnables(float DeltaTime)
{
	CurrentlyBurning.Empty();
	for (int32& id : BurnableComponents)
	{
		if (Burnables[id]->IsBurning)
			CurrentlyBurning.Add(id);
	}

	//Update distance to closest fire, for all burnables that are not already on fire
	DistanceToFire.Empty();
	for (int32& burnableId : BurnableComponents)
	{
		UBSBurnableComponent* burnable = Burnables[burnableId];
		if (burnable->IsBurning)
			continue;

		int32 closestBurning = -1;
		float closestFire = BIG_DISTANCE;
		for (int32& burningId : CurrentlyBurning)
		{
			UBSBurnableComponent* burning = Burnables[burningId];
			const float distance = (burnable->GetOwner()->GetActorLocation() - burning->GetOwner()->GetActorLocation()).Size();
			if(distance < closestFire)
			{
				closestFire = distance;
				closestBurning = burning->ComponentID;
			}
		}
		DistanceWithId temp;
		temp.Id = closestBurning;
		temp.Distance = closestFire;
		DistanceToFire.Add(burnable->ComponentID, temp);
	}

	for (int32& burnableId : BurnableComponents)
	{
		//Check if burnable should start or stop igniting
		if (DistanceToFire.Contains(burnableId))
		{
			DistanceWithId IdDistance = DistanceToFire[burnableId];
			if (CurrentlyBurning.Contains(IdDistance.Id))
			{
				const float radius = Burnables[IdDistance.Id]->FireSpreadRadius;

				if (IdDistance.Distance < radius)
				{
					if (!IgnitionTimers.Contains(burnableId))
					{
						IgnitionTimers.Add(burnableId, 0.0f);
						if (Burnables[burnableId]->OnStartedIgnition.IsBound())
						{
							Burnables[burnableId]->OnStartedIgnition.Broadcast();
						}
					}
				}
				else if (IgnitionTimers.Contains(burnableId))
				{
					IgnitionTimers.Remove(burnableId);

					if (Burnables[burnableId]->OnStoppedIgnition.IsBound())
					{
						Burnables[burnableId]->OnStoppedIgnition.Broadcast();
					}
				}
			}
		}
		//Check if ignition should finish
		if (IgnitionTimers.Contains(burnableId))
		{
			IgnitionTimers[burnableId] += DeltaTime;
			if (IgnitionTimers[burnableId] >= Burnables[burnableId]->TimeUntilBurning)
			{
				if (Burnables[burnableId]->OnStartedBurning.IsBound())
				{
					Burnables[burnableId]->OnStartedBurning.Broadcast();
				}
				
				Burnables[burnableId]->IsBurning = true;
				IgnitionTimers.Remove(burnableId);
				if(!DeathTimers.Contains(burnableId))
					DeathTimers.Add(burnableId, 0.0f);
			}
		}
		if (DeathTimers.Contains(burnableId))
		{
			DeathTimers[burnableId] += DeltaTime;
			if (DeathTimers[burnableId] > Burnables[burnableId]->TimeUntilDeath)
			{
				if (Burnables[burnableId]->OnBurnedDown.IsBound())
				{
					Burnables[burnableId]->OnBurnedDown.Broadcast();
				}
				ComponentsToDestroy.Add(burnableId);
				
			}
		}
	}

	for (int32 id : ComponentsToDestroy)
		RemoveComponent(id);

}

void AElementComponentSystem::UpateExplosives(float DeltaTime)
{
	TArray< int32> Exploding;
	for (int32& id : ExplosiveComponents)
	{
		if(!Explosives[id]->IsActivated)
			continue;

		for (int32& burningId : CurrentlyBurning)
		{
			const float distance = (Explosives[id]->GetOwner()->GetActorLocation() - Burnables[burningId]->GetOwner()->GetActorLocation()).Size();
			if (distance < Burnables[burningId]->FireSpreadRadius)
				Explosives[id]->IsPrimed = true;
		}
		if(Explosives[id]->IsPrimed)
		{
			if (!PrimeTimers.Contains(id))
			{
				PrimeTimers.Add(id, 0.0f);
				if (Explosives[id]->OnFuseIgnition.IsBound())
					Explosives[id]->OnFuseIgnition.Broadcast();
			}
		}
		if (PrimeTimers.Contains(id))
		{
			PrimeTimers[id] += DeltaTime;
			if (PrimeTimers[id] >= Explosives[id]->FuseTime)
				Exploding.Add(id);
		}
	}

	for (int32& id : Exploding)
	{
		if(ComponentsToDestroy.Contains(id)) continue;
		ComponentsToDestroy.Add(id);

		if (!Explosives[id]->OnExplosion.IsBound())
			continue;

		FVector epicenter = Explosives[id]->GetOwner()->GetActorLocation();
		float radius = Explosives[id]->BlastRadius;
		float power = Explosives[id]->ExplosivePower;
		int32 explodingComponentsNum = 1;
		Explosives.ValueSort([epicenter](UBSExplosiveComponent& a, UBSExplosiveComponent& b)
		{
			const float aDistance = (a.GetOwner()->GetActorLocation() - epicenter).SizeSquared();
			const float bDistance = (b.GetOwner()->GetActorLocation() - epicenter).SizeSquared();
			return aDistance < bDistance;
		});
		for (auto pair : Explosives)
		{
			if(pair.Key == id) continue;
			const FVector otherLocation = pair.Value->GetOwner()->GetActorLocation();
			const FVector toOther = otherLocation - (epicenter / explodingComponentsNum);
			if (toOther.SizeSquared() > radius * radius) break;
			if (!pair.Value->IsActivated) continue;
			explodingComponentsNum++;
			epicenter += pair.Value->GetOwner()->GetActorLocation();
			radius += pair.Value->BlastRadius;
			power += pair.Value->ExplosivePower;
			ComponentsToDestroy.Add(pair.Key);
		}
		Explosives[id]->OnExplosion.Broadcast(epicenter / explodingComponentsNum, radius, power);
	}

	for (int32 id : ComponentsToDestroy)
		RemoveComponent(id);

}
       
   

Responsibilities


Components:

    • Burnable
    • Explosive
    • Explodable

Systems:

    • Component system

Misc:

    • Source control assistance (Perforce)

Components:


Burnable/Explodable/Explosive

Created a component for all the things in the world that is supposed to be able to be set on fire, exlpode or be blown up so that the designers could just attach a component to the assets that needed any of the traits.


The components only held the data that later got transformed in the component sytem.

Systems:


Component system

Together with an other programmer we created a system that handles all the components. The system started of simple with checking distances between components and checking if they could interact with each other.

This proved to be very performance heavy when we had many components.
This led to us rewriting the system and implement unreals octtree to improve performance on the system.