• Platform
  • Language
  • Engine
  • Development Time
  • Team Size
  • Graphics API
  • Windows
  • C#
  • Unity 2019.3.0(Alpha4b)
  • 7 Weeks
  • 12
  • DirectX12 Unity HDRP

project name

Round Creation Tool

Design Request:

  • Rounds with scaling difficulty
  • Waves in those rounds
  • Show popup text (tutorials, hints)
  • Set/Pause/stop rounds and waves(save points)
  • Play sounds
    • At Round Start
    • At Wave Start
    • At difficulty change
    • At any time (To indicate a boss fight)
  • Spawn Enemies
    • At what spawnpoint?
    • What type of enemy?
    • How many?
    • Should the next wave wait until the enemies of the current wave are dead?

                                        
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.AI;

[CustomEditor(typeof(Round))]
public class RoundScriptableEdit : Editor
{
    Round round;
    SerializedObject GetTarget;
    SerializedProperty list;

    RoundController.slotFunction SlotFunction;

    int listSize = 0;

    GUIStyle textStyle = new GUIStyle();
    GUIStyle spawnerStyle = new GUIStyle();
    GUIStyle soundStyle = new GUIStyle();
    GUIStyle waitStyle = new GUIStyle();
    GUIStyle waveStyle = new GUIStyle();
    GUIStyle OpenUIStyle = new GUIStyle();
    GUIStyle ShowScoreStyle = new GUIStyle();

    void OnEnable()
    {
        round = (Round)target;
        GetTarget = new SerializedObject(round);
        list = GetTarget.FindProperty("slots");

        textStyle.alignment = TextAnchor.MiddleCenter;
        textStyle.fontStyle = FontStyle.Bold;
        textStyle.normal.textColor = Color.white;

        spawnerStyle.alignment = TextAnchor.MiddleCenter;
        spawnerStyle.normal.textColor = Color.cyan;

        soundStyle.alignment = TextAnchor.MiddleCenter;
        soundStyle.normal.textColor = Color.yellow;

        waitStyle.alignment = TextAnchor.MiddleCenter;
        waitStyle.normal.textColor = Color.white;

        waveStyle.alignment = TextAnchor.MiddleCenter;
        waveStyle.normal.textColor = Color.red;

        OpenUIStyle.alignment = TextAnchor.MiddleCenter;
        OpenUIStyle.normal.textColor = Color.magenta;

        ShowScoreStyle.alignment = TextAnchor.MiddleCenter;
        ShowScoreStyle.normal.textColor = Color.green;

    }

    public override void OnInspectorGUI()
    {
        GetTarget.Update();

        EditorGUILayout.Space();
        listSize = list.arraySize;

        EditorGUILayout.LabelField("Round Music", textStyle);
        SerializedProperty MySong = GetTarget.FindProperty("Song");
        EditorGUILayout.PropertyField(MySong);
        EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);

        EditorGUILayout.LabelField("ThreatLevel", textStyle);
        SerializedProperty MyThreat = GetTarget.FindProperty("ThreatLevel");
        EditorGUILayout.PropertyField(MyThreat);
        EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);


        listSize = EditorGUILayout.IntField("Round Events:", listSize);

        if (listSize < 1)
        {
            listSize = 1;
        }

        if (listSize != list.arraySize)
        {
            while (listSize > list.arraySize)
            {
                list.InsertArrayElementAtIndex(list.arraySize);
            }
            while (listSize < list.arraySize)
            {
                list.DeleteArrayElementAtIndex(list.arraySize - 1);
            }
        }
        EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);

        //Foreach slot in our Round
        for (int i = 0; i < list.arraySize; i++)
        {
            //Get our values from base class
            SerializedProperty MyListRef = list.GetArrayElementAtIndex(i);
            SerializedProperty MyFunction = MyListRef.FindPropertyRelative("function");
            SerializedProperty MyEnemyType = MyListRef.FindPropertyRelative("EnemyType");
            SerializedProperty MySpawnPoint = MyListRef.FindPropertyRelative("SpawnPosition");
            SerializedProperty MySpawnAmount = MyListRef.FindPropertyRelative("SpawnAmount");
            SerializedProperty MySpawnTime = MyListRef.FindPropertyRelative("SpawnTime");
            SerializedProperty MyWaitTime = MyListRef.FindPropertyRelative("WaitFor");
            SerializedProperty MySound = MyListRef.FindPropertyRelative("EventRef");
            SerializedProperty MyMessage = MyListRef.FindPropertyRelative("message");
            SerializedProperty MySprite = MyListRef.FindPropertyRelative("sprite");
            SerializedProperty MyType = MyListRef.FindPropertyRelative("type");

            //Show different editable values depending on the function of the slot.
            switch (MyListRef.FindPropertyRelative("function").enumValueIndex)
            {
                case (int)RoundController.slotFunction.AiSpawn:
                    GUILayout.BeginHorizontal();
                    EditorGUILayout.LabelField("Ai Spawn", spawnerStyle);
                    EditorGUILayout.PropertyField(MyFunction, GUIContent.none, false);
                    GUILayout.EndHorizontal();
                    EditorGUILayout.Space();
                    EditorGUILayout.PropertyField(MyEnemyType);
                    EditorGUILayout.PropertyField(MySpawnPoint);
                    EditorGUILayout.PropertyField(MySpawnAmount);

                    if (MySpawnAmount.intValue < 1)
                    {
                        MySpawnAmount.intValue = 1;
                    }
                    if (MySpawnTime.floatValue < i && MySpawnTime.floatValue > i)
                     {
                         MySpawnTime.floatValue = i;
                     }
                    break;

                case (int)RoundController.slotFunction.PauseRoundTime:
                    EditorGUILayout.BeginHorizontal();
                    EditorGUILayout.LabelField("Delay Round(Time)", waitStyle);
                    EditorGUILayout.PropertyField(MyFunction, GUIContent.none, false);
                    GUILayout.EndHorizontal();
                    EditorGUILayout.Space();
                    EditorGUILayout.PropertyField(MyWaitTime);
                    if (MySpawnTime.floatValue < i && MySpawnTime.floatValue > i)
                    {
                        MySpawnTime.floatValue = i;
                    }
                    break;

                case (int)RoundController.slotFunction.WaitForAiClear:
                    EditorGUILayout.BeginHorizontal();
                    EditorGUILayout.LabelField("Pause For Ai Clear", waveStyle);
                    EditorGUILayout.PropertyField(MyFunction, GUIContent.none, false);
                    GUILayout.EndHorizontal();
                    EditorGUILayout.Space();
                    if (MySpawnTime.floatValue < i && MySpawnTime.floatValue > i)
                    {
                        MySpawnTime.floatValue = i;
                    }
                    break;

                case (int)RoundController.slotFunction.Popup:
                    EditorGUILayout.BeginHorizontal();
                    EditorGUILayout.LabelField("Popup/Hint", OpenUIStyle);
                    EditorGUILayout.PropertyField(MyFunction, GUIContent.none, false);
                    GUILayout.EndHorizontal();
                    EditorGUILayout.Space();
                    EditorGUILayout.PropertyField(MyMessage);
                    EditorGUILayout.PropertyField(MySprite);
                    EditorGUILayout.PropertyField(MyType);
                    if (MySpawnTime.floatValue < i && MySpawnTime.floatValue > i)
                    {
                        MySpawnTime.floatValue = i;
                    }
                    break;


                case (int)RoundController.slotFunction.ShowScore:
                    EditorGUILayout.BeginHorizontal();
                    EditorGUILayout.LabelField("Show Scoreboard", ShowScoreStyle);
                    EditorGUILayout.PropertyField(MyFunction, GUIContent.none, false);
                    GUILayout.EndHorizontal();
                    EditorGUILayout.Space();

                    if (MySpawnTime.floatValue < i && MySpawnTime.floatValue > i)
                    {
                        MySpawnTime.floatValue = i;
                    }
                    break;

                default:
                    EditorGUILayout.BeginHorizontal();
                    EditorGUILayout.LabelField("Play Sound", soundStyle);
                    EditorGUILayout.PropertyField(MyFunction, GUIContent.none, false);
                    GUILayout.EndHorizontal();
                    EditorGUILayout.Space();
                    EditorGUILayout.PropertyField(MySound);
                    if (MySpawnTime.floatValue < i && MySpawnTime.floatValue > i)
                    {
                        MySpawnTime.floatValue = i;
                    }
                    break;
            }

            GUILayout.BeginHorizontal();
            if (GUILayout.Button("Up"))
            {
                if (i - 1 > -1)
                {
                    list.GetArrayElementAtIndex(i).FindPropertyRelative("SpawnTime").floatValue -= 1.01f;
                }
            }

            if (GUILayout.Button("Remove"))
                list.DeleteArrayElementAtIndex(i);
            if (GUILayout.Button("Down"))
                if(i + 1 <= list.arraySize)
                    list.GetArrayElementAtIndex(i).FindPropertyRelative("SpawnTime").floatValue += 1.01f;
            GUILayout.EndHorizontal();

            EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
        }
        //Apply the changes to our list
        GetTarget.ApplyModifiedProperties();

        //Sort the slots after the spawn time. Linq leaks memory but should be fine in a editor scenario
        round.slots = round.slots.OrderBy(go => go.SpawnTime).ToList();

    }

}
                                        
                                    

Ai Creation

With a short deadline, corners were cut and my implementation of our three enemies had to be fast. So a well structured StateMachine following my flowchart had to do.
Using generic behaviors/classes, I could reuse must of the scripts for all of our enemies. I only added specific attack behaviors and some pathing behavior to seal the deal.
I needed to save on performance so instead of adding a flocking behavior, I added a node system for player-aggro and the main objective (The nexus). So each Ai was handed a path to a point around the target to reduced clumping of the Ai.



Making Rhythm A Focus

A design goal for the team with the AI was to make them act to the tempo in the music. So I added many subtle behaviors to convey this idea.

  • Attacks are synchronize to the beat of the music
  • Animations are started on beat.
    • Most animations are 15 frames or 0.25sec for a 90 tempo beat. So they also finish on beat!

The same goes for my implementation of our characters ultimate ability. On input I set our character to a ultimate-state. After 1 beat(0.25seconds) I spawn the vfx to all our affected enemies and sets them into a stun-state. The next beat the vfx strikes and damage is applied.

                                        
#pragma warning disable 0649
using UnityEngine;
using System;
using StateKraft;
using UnityEngine.AI;

public class AiController : MonoBehaviour, IDamageable
{
    [Header("EnemyType:")]
    public Enemy EnemyType;

    [Header("Health Values:")]
    public float MaxHp = 100f;
    public float Hp;
    public Action OnDeath;
    public Action OnDamage;

    [Header("Navigation:")]
    public NpcSlot CurrentSlot;

    [Header("Knockback Values:")]
    public RhythmGrade GradeRequiredToKnockback;

    [Header("Components/refrences:")]
    public GameObject ChargeAttackVFX;
    public Animator Anime;
    public NavMeshAgent Agent;
    public Transform FirePoint;
    public BoxCollider AttackCollider;
    public static Transform Player;
    public NexusCrowdManager Nexus;

    [Header("System:")]
    public StateMachine Machine;

    [Header("Debug:")]
    [SerializeField] Vector3 gizmoOffset = Vector3.up;
    [SerializeField, Range(0f, 3f)] float gizmoSize = 0.1f;

    [SerializeField] private MaterialFloatLerper _hurtLerper;
    public Renderer Renderer;

    private void Awake()
    {
        Nexus = NexusCrowdManager.Instance;
        Anime = GetComponentInChildren<,Animator>();
        Agent = GetComponent<,NavMeshAgent>();
        if (Player == null)
        {
            Player = FindObjectOfType<,PlayerEngine>().transform;
        }
        Machine.Initialize(this);
        if(_hurtLerper == null)
            _hurtLerper = GetComponentInChildren<,MaterialFloatLerper>();
    }

    public void Setup(Vector3 startPos)
    {
        CurrentSlot = Nexus.RequestSlot(startPos);
        Agent.enabled = true;
        Hp = MaxHp;
        Agent.Warp(startPos);
        Machine.TransitionTo<,AiWalking>();
    }

    private void Update()
    {
        if (PauseGame.IsPaused())
            return;

        Machine.Update();
        CheckIfInBounds();
    }

    public void TakeDamage(int damage = 1,RhythmGrade grade = RhythmGrade.Miss,
                                                            Transform attacker = null, float MissMultiplier = 1)
    {
        if(_hurtLerper)
            _hurtLerper.LerpValue(true);

        if (grade == RhythmGrade.Miss && MissMultiplier == 0)
            return;

        if ((int)grade > 0)
            PopUpPointsHandler.Instance.OnEnemyHitSpawn(transform.position, grade);

        Hp -= grade == RhythmGrade.Miss ? damage * MissMultiplier : damage;

        OnDamage?.Invoke();

        if (Hp <= 0)
            Die();

        else if (!(Machine.CurrentState is AiCoreAttackState))
        {
            if (CurrentSlot.playerIndex >= 0 && CurrentSlot.playerIndex <= 3)
            {
                Machine.GetState<,AiWalking>().shouldFreeSlot = false;
            }

            if ((int)grade > (int)GradeRequiredToKnockback)
            {
                if(EnemyType != Enemy.shooterAi)
                    Machine.GetState<,AiKnockbackState>().TriggerHeavyKnockBack = true;
            }

            if(EnemyType == Enemy.basicAi)
                Machine.TransitionTo<,AiKnockbackState>();
        }
    }

    public void Die()
    {
        Player.GetComponent<,AiSlotMachine>().SetSlotFree(CurrentSlot.playerIndex);
        if (CurrentSlot.nexusIndex != -1)
            Nexus.NpcSlots[CurrentSlot.nexusIndex].taken = false;
        Machine.TransitionTo<,AiDeath>();
    }

    //Removed on release build
    private void OnDrawGizmos()
    {
        if (Machine.CurrentState)
        {
            Gizmos.color = Machine.CurrentState.StateGizmoColor;
            Gizmos.DrawSphere(transform.position + gizmoOffset, gizmoSize);
        }
    }

    private void CheckIfInBounds()
    {
        if (transform.position.y < -100f)
        {
            Debug.LogError("AI fell through map!");
            Die();
        }
    }

}
}
                                        
                                    


Deathtrap Component

All of our traps in the game are driven by designer blueprints that are connect to my DeathTrapComponent. The component calculates direction of hits and forwards information about the kills to a log, that on level completion is shown to the player. The designer can choice what type of collider it wants to connect to the component. So we were able to use the same component for all of our "killables" in the levels.
The component can also take damage. To the left you can see an example of an orc exploding and destroying the component

                                        
#include "BSDeathTrapComponent.h"
#include "Core/BSAIAgent.h"

UBSDeathTrapComponent::UBSDeathTrapComponent()
{
	PrimaryComponentTick.bCanEverTick = false;
}

int32 UBSDeathTrapComponent::GetDeathCount() const
{
	return deathCount;
}

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

	for (int i = 0; i < DeathColliders.Num(); i++)
	{
		DeathColliders[i]->OnComponentBeginOverlap.AddDynamic(this, &UBSDeathTrapComponent::BeginOverlap);
		DeathColliders[i]->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
		DeathColliders[i]->SetCollisionProfileName(FName("DeathComponent"));
	}
}

void UBSDeathTrapComponent::BeginOverlap(UPrimitiveComponent* OverlappedComponent,
                                AActor* OtherActor, UPrimitiveComponent* OtherComponent, int32 OtherBodyIndex, bool bFrameSweep, const FHitResult& SweepResults)
{
	if (trapActive)
	{
		if (ABSAIAgent* Orc = Cast(OtherActor))
		{
			if (Orc->IsDead() != true)
			{
				Orc->Die(DeathType);
				deathCount++;
				OnKill.Broadcast(OtherActor, SweepResults.ImpactPoint);
			}
		}
	}
}
}
                                        
                                    

Catapult

Events are rigged up to give the designer the tools they need to send some orcs to heaven. For example we have events for Catapult-Recharge, Catapult-Fire, Catapult Ready.
The destination target is set by an Unreal Target Actors. A curve is then calculated with a ProjectileComponent with a random offset.

                                        
#include "BSCatapultPreasure.h"
#include "Components/BSSelectableComponent.h"
#include

ABSCatapultPreasure::ABSCatapultPreasure()
{
	PrimaryActorTick.bCanEverTick = true;

}

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

	if (Collider != nullptr)
	{
		Collider->OnComponentBeginOverlap.AddDynamic(this, &ABSCatapultPreasure::BeginOverlap);
		Collider->OnComponentEndOverlap.AddDynamic(this, &ABSCatapultPreasure::EndOverlap);
		Collider->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
		Collider->SetCollisionProfileName(FName("DeathComponent"));

		FTimerManager& TimerManager = GetWorldTimerManager();
		TimerManager.ClearTimer(untilNextLaunch);
		TimerManager.ClearTimer(delayedDisable);
	}
}

void ABSCatapultPreasure::BeginOverlap(UPrimitiveComponent* OverlappedComponent,
                                 AActor* OtherActor, UPrimitiveComponent* OtherComponent,
                                 int32 OtherBodyIndex, bool bFrameSweep, const FHitResult& SweepResults)
{
	if (Active && Target != nullptr)
	{
		if (ABSAIAgent* Orc = Cast(OtherActor))
		{
			if (Orc->IsDead() != true)
			{
				FTimerManager& TimerManager = GetWorldTimerManager();

				FVector offset = Collider->GetCenterOfMass() +=
                                 FVector(FMath::RandRange(-orcSpacing, orcSpacing), 0.0f,
                                 FMath::RandRange(-orcSpacing, orcSpacing));

				if (TimerManager.GetTimerRemaining(untilNextLaunch) <= 0.0f)
				{
					TimerManager.ClearTimer(untilNextLaunch);
					TimerManager.SetTimer(untilNextLaunch, this, &ABSCatapultPreasure::Launch, Time, false);

					if (OnDelayStart.IsBound())
						OnDelayStart.Broadcast();
				}

				if(!agentList.Contains(Orc))
				{
					agentList.Add(Orc);
				}

			}
		}
	}
}

void ABSCatapultPreasure::Launch()
{

	for (int i = 0; i < agentList.Num(); i++)
	{
		if (UBSProjectileComponent* ProjectileComponent = agentList[i]->GetProjectileComp())
		{
			if (ProjectileComponent->Activated != true)
			{
				ProjectileComponent->SetTarget(Target->GetActorLocation());
			}
		}

	}
	agentList.Empty();

	if (OnProjectileFire.IsBound())
		OnProjectileFire.Broadcast();
}

void ABSCatapultPreasure::EndOverlap(UPrimitiveComponent* OverlappedComponent,
                                            AActor* OtherActor, UPrimitiveComponent* OtherComponent, int32 OtherBodyIndex)
{
	if (ABSAIAgent* Orc = Cast(OtherActor))
	{
		agentList.Remove(Orc);
		Orc->StopMovement();

	}
}

  //NEW CLASS
UBSProjectileComponent::UBSProjectileComponent()
{
	PrimaryComponentTick.bCanEverTick = true;

}
void UBSProjectileComponent::BeginPlay()
{
	Super::BeginPlay();
}
void UBSProjectileComponent::TickComponent(float DeltaTime,
                                            ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (!Activated) return;

	Velocity -= FVector::UpVector * Gravity * DeltaTime;
	FHitResult hit;
	GetOwner()->AddActorWorldOffset(Velocity * DeltaTime, true, &hit);

	if (hit.bBlockingHit)
	{
		if (OnProjectileHit.IsBound())
			OnProjectileHit.Broadcast(hit, Velocity);


		if (ABSAIAgent* Orc = Cast<,ABSAIAgent>(GetOwner()))
		{
			Orc->ActivateUnit();


			if (UBSSelectableComponent* selectComp = Orc->GetSelectableComp())
			{
				selectComp->ReActivate();
			}
		}

		Activated = false;
	}


}

void UBSProjectileComponent::SetTarget(const FVector& worldLocation)
{
	const FVector Spacinglocation = worldLocation + FVector(FMath::RandRange(0.0f, Accuracy),
                                            FMath::RandRange(0.0f, Accuracy),0.0f);

	TargetLocation = Spacinglocation;
	StartLocation = GetOwner()->GetActorLocation();
	FVector toLocation = TargetLocation - StartLocation;
	const float deltaZ = TargetLocation.Z - StartLocation.Z;
	toLocation.Z = 0.0f;
	const float planarDistance = toLocation.Size2D();
	TrajectoryRotation = toLocation.Rotation();


	AngleInRads = FMath::Atan((deltaZ + 0.5f * Gravity * TimeToImpact * TimeToImpact) / planarDistance);
	InitialVelocity = planarDistance / (TimeToImpact * FMath::Cos(AngleInRads));
	TrajectoryRotation.Pitch = FMath::RadiansToDegrees(AngleInRads);
	Velocity = TrajectoryRotation.Vector() * InitialVelocity;

	Activated = true;

	if (OnProjectileFire.IsBound())
		OnProjectileFire.Broadcast(TrajectoryRotation);
}




                                        
                                    

CameraShake

I got distracted and implemented DestructableMeshes from the Apex Physic Engine, then I realized we need a damn CameraShake. With us having a lot happening on the screen at once. I thought it was a good idea to make the CameraShaker addition based. So if ten Orcs explode at the same time. They will add shake on top of each other.
This turned out great and gave our designers more time to focus on other things then tweaking "large explosion" shake values. With some added roll to the shake I also achieved a cartony feel.

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

	//Decrease CurrentIntensity every deltaframe
	CurrentIntensity -= DeltaTime / MaxDuration;
	CurrentIntensity = FMath::Clamp(CurrentIntensity, 0.0f, 1.0f);
	//Also clamp it 0-1 so ppl cant go overboard

	//Get xPerlin and multiple with how fast we want the shake to be
	float xPerlin = (UKismetMathLibrary::PerlinNoise1D(World->GetRealTimeSeconds() * PerlinSpeed.X) + 1) * 0.5f;
	float xShake = FMath::Lerp(-MaxShake.X, MaxShake.X, xPerlin);

	float yPerlin = (UKismetMathLibrary::PerlinNoise1D(World->GetRealTimeSeconds() * PerlinSpeed.Y) + 1) * 0.5f;
	float yShake = FMath::Lerp(-MaxShake.Y, MaxShake.Y, yPerlin);

	float zPerlin = (UKismetMathLibrary::PerlinNoise1D(World->GetRealTimeSeconds() * PerlinSpeed.Z) + 1) * 0.5f;
	float zShake = FMath::Lerp(-MaxShake.Z, MaxShake.Z, zPerlin);

	//For Rotation Roll
	float rollIntense = (UKismetMathLibrary::PerlinNoise1D((World->GetRealTimeSeconds() * RollSpeed)) + 1) * 0.5f;
	float rollShake = FMath::Lerp(-MaxRoll, MaxRoll, rollIntense);

	//Set our cameracomp to it
	if (CameraRef)
	{
		CameraRef->SetRelativeLocation(FVector(xShake * CurrentIntensity,
                                 yShake * CurrentIntensity, zShake * CurrentIntensity));
		CameraRef->SetRelativeRotation(FRotator(0.0f, 0.0f, rollShake * CurrentIntensity));
	}

}
                                        
                                    

The Orc Ai

Events are rigged up to give the designer the tools they need to send some orcs to heaven. For example we have events for Catapult-Recharge, Catapult-Fire, Catapult Ready. The destination target is set by an Unreal Target Actor.
I did not want to use boids as we wanted to keep the horde as predictable as possible. So I ended up giving the orcs target location by drawing up a circle around the mouse click position. With some added noise it turned out great and predictable.

                                        
void ABSPlayerController::SetMoveLocation()
{
	TimeOfLastMoveCommand = GetWorld()->GetRealTimeSeconds();
	const int32 size = Selectables.Num();
	if (size == 0)
		return;

	FVector worldLocation;
	FVector worldDirection;
	DeprojectMousePositionToWorld(worldLocation, worldDirection);
	FHitResult hit;

    //Get MoveClick position
	GetWorld()->LineTraceSingleByChannel(hit, worldLocation, worldLocation + worldDirection * 10000, ECC_Visibility);
	if (!hit.bBlockingHit)
		return;


	//Spawn graphics for click
	if (MoveCommandIndicator != nullptr && ShowMovementIndicator)
	{
		FVector location = hit.ImpactPoint;
		GetWorld()->SpawnActor(MoveCommandIndicator, &location);
		ShowMovementIndicator = false;
	}

     //Create default values for stepping in a circle around mouse click position
	FVector Location;
	const FVector targetlocation = hit.Location;
	currentDistance = 200.f;
	currentAngle = 0.f;
	angleStep = 40.0f;

    //Foreach orc selected to this
	for (int32 i = 0; i < size;)
	{
		if (i == 0)
		{
			Location = targetlocation;
			if (Selectables[i]->OnMoveCommand.IsBound())
				Selectables[i]->OnMoveCommand.Broadcast(Location);
			i++;
			continue;
		}

		FVector ProjectedLocation;
		FVector Location = targetlocation + FVector((currentDistance * FMath::RandRange(0.9f, 1.2f)) *
                                 FMath::Sin(FMath::DegreesToRadians(currentAngle)), currentDistance *
                                 FMath::Cos(FMath::DegreesToRadians(currentAngle)), 0);
		currentAngle += angleStep;
		if (currentAngle > 360 - angleStep)
		{
			currentAngle -= 360 - angleStep;
			currentDistance += DISTANCE_STEP;
			angleStep *= AngleFactor;
		}

		if (!UNavigationSystemV1::K2_ProjectPointToNavigation(GetWorld(), Location,
                                 ProjectedLocation, nullptr, TSubclassOf()))
			continue;

		if (Selectables[i]->OnMoveCommand.IsBound())
			Selectables[i]->OnMoveCommand.Broadcast(Location);
		i++;
	}
}
                                        
                                    

Panic!

I gave the Orc's a panic-mode when they are sat on fire, and this led to a lot of funny interactions. As our component system interacts with water and traps in the world (as seen to the left). They are given a random direction to run towards and the function will be recalled until they die or if the fired is put out.

                                        
void ABSAIAgent::PanicMode()
{

	UBSSelectableComponent* selectableComp = Cast(GetComponentByClass(UBSSelectableComponent::StaticClass()));
	UBSBurnableComponent* burnableComp = Cast(GetComponentByClass(UBSBurnableComponent::StaticClass()));

	if (selectableComp && burnableComp)
	{
		if (selectableComp->IsSelected || !burnableComp->IsBurning)
			return;

		currentAngle = FMath::RandRange(0.0f, 360.0f);

		FVector Location = GetActorLocation() + FVector((BurnPanicDistance *
                                     FMath::RandRange(minMaxFactor.x, minMaxFactor.y)) *
                                     FMath::Sin(FMath::DegreesToRadians(currentAngle)), BurnPanicDistance *
                                     FMath::Cos(FMath::DegreesToRadians(currentAngle)), 0);

		if (UCharacterMovementComponent* CharMovementComp =
                                     Cast(GetComponentByClass(UCharacterMovementComponent::StaticClass())))
			CharMovementComp->bRequestedMoveUseAcceleration = false;

		MoveToLocation(Location);

		FTimerManager& TimerManager = GetWorldTimerManager();
		TimerManager.ClearTimer(circleTick);
		TimerManager.SetTimer(circleTick, this, &ABSAIAgent::PanicMode, 0.5f, false);
	}
}