Simon Larsson

Simon Larsson

Game Programmer

  • 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);
	}
}
                                        
                                    


Npc ID System

To keep Client and Server communication clean. I made the Npc system and most of the other systems ID based. An ID is created by both the client/server when a new npc is spawned into the world. I then loop through all the npc on the server to update their position. If a position is changed... the ID and the new position is sent to the client.
Same thing is going on Client side more or less. The Client loops through a list of the IDs checking for changes and updates their visualisation.

                                        
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Netkraft;
using Netkraft.Messaging;
using UnityEngine;

[System.Serializable]
public class ServerNpcManager
{
    [SerializeField] private Server _server;
    [SerializeField] private Grid _grid;
    [SerializeField] private Ship _ship;

    private readonly List _npcs = new List();
    private int _idCounter = 0;



    public void Update()
    {
        float dt = Time.deltaTime;
        foreach (ServerNpc npc in _npcs)
        {
            if (npc.Path.Count > 1)
            {
                npc.timer += dt;

                if (npc.timer < 1f)
                    continue;

                //Remove completed Targetcell
                _grid.OccupyCell(npc.TargetCell, false);
                npc.Path.RemoveAt(npc.Path.Count-1);
                npc.timer = 0;

                //If we have more Targetcells dont skip
                if (npc.Path.Count < 1)
                    continue;

                //Set new TargetCell
                npc.TargetCell = npc.Path[npc.Path.Count-1];
                _grid.OccupyCell(npc.TargetCell, true);
            }
        }

        Server.Send(new NpcUpdateMessage {ServerNpcs = _npcs.ToArray()});
    }

    public void AddNpc(Vector2Int gridPosition)
    {
        _idCounter++;
        _npcs.Add(new ServerNpc {Id = _idCounter, TargetCell = gridPosition});
        Debug.Log("Added Npc with ID: " + _idCounter);
    }

    public bool AddPathToNpc(int npcId, Vector2Int goal)
    {
        foreach (ServerNpc npc in _npcs)
        {
            if (npc.Id != npcId)
                continue;

            npc.Path = ServerAstar.GetPath(npc.TargetCell, goal).ToList();
            if (npc.Path.Count != 0)
            {
                npc.TargetCell = npc.Path[npc.Path.Count - 1];
                Debug.Log("Server added path!");
                return true;
            }
        }

        Debug.Log("Server could not find Npc ID! Or Path was not reachable!");
        return false;
    }

    [Writable]
    public class ServerNpc
    {
        public int Id;
        public Vector2Int TargetCell;
        [SkipIndex] public float timer;
        [SkipIndex] public List Path = new List();
    }
}
                                        
                                    

Pathfinding A*

When a path is requested, the server creates a path with an A* algorithm. Said path is only stored on the server, and the path is evaluated by the server with a timer for each node in the path array.
The Clients only receive an update object position to lerp towards. This works for all object in the game.

                                        
using System;
using System.Collections.Generic;
using System.Linq;
using Netkraft.Messaging;
using UnityEngine;

public static class ServerAstar
{
    private static Grid _grid;
    private static List _openList = new List();
    private static List _closedList = new List();
    private static Node _current;
    private static List _adjacencies;


    public static void Init(Grid grid)
    {
        _grid = grid;
    }

    public static Vector2Int[] GetPath(Vector2Int gridStart, Vector2Int gridEnd)
    {
        _openList.Clear();
        _closedList.Clear();
        List path = new List();
        Node start = new Node(gridStart);
        _openList.Add(start);

        while (_openList.Count > 0 && !_closedList.Exists(x => x.Position == gridEnd))
        {
            _current = _openList[0];
            _openList.Remove(_current);
            _closedList.Add(_current);
            _adjacencies = GetAdjacentNodes(_current);

            foreach (Node n in _adjacencies)
            {
                if(_closedList.Contains(n) || _openList.Contains(n))
                    continue;;

                n.Parent = _current;
                n.DistanceToGoal = ManhattanDistance(n.Position, gridEnd);
                n.Cost = 1 + n.Parent.Cost;
                _openList.Add(n);
                _openList = _openList.OrderBy(node => node.F).ToList();
            }
        }

        if (!_closedList.Exists(x => x.Position == gridEnd))
        {
            Debug.Log("Could not find a Path!");
            return null;
        }

        Node currentNode = _closedList[_closedList.IndexOf(_current)];

        while (currentNode.Parent != start && currentNode != null)
        {
            path.Insert(0, currentNode);
            currentNode = currentNode.Parent;
        }

        List temp = new List();

        foreach (Node x in path)
        {
            temp.Add(x.Position);
        }

        return temp.ToArray();
    }

    private static List GetAdjacentNodes(Node n)
    {
        List temp = new List();

        if(n.Position.y + 1 <= _grid.Height)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(0, 1)))
                temp.Add(new Node(new Vector2Int(n.Position.x, n.Position.y + 1)));
        }
        if(n.Position.y - 1 >= 0)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(0, -1)))
                temp.Add(new Node(new Vector2Int(n.Position.x, n.Position.y - 1)));
        }
        if(n.Position.x - 1 >= 0)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(-1, 0)))
                temp.Add(new Node(new Vector2Int(n.Position.x - 1, n.Position.y)));
        }
        if(n.Position.x + 1 <= _grid.Width)
        {
            if(_grid.ValidMove(_grid.GetCell(n.Position), new Vector2Int(1, 0)))
                temp.Add(new Node(new Vector2Int(n.Position.x + 1, n.Position.y)));
        }


        return temp;
    }
    private static int ManhattanDistance(Vector2Int start, Vector2Int end)
    {
        checked {
            return Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y);
        }
    }

}

[Writable, Serializable]
public class Node
{
    public Vector2Int Position;
    public int DistanceToGoal;
    public int Cost;
    public Node Parent;
    public float F
    {
        get
        {
            if (DistanceToGoal != -1 && Cost != -1)
                return DistanceToGoal + Cost;
            else
                return -1;
        }
    }

    public Node(Vector2Int start)
    {
        Position = start;
        Cost = 1;
        DistanceToGoal = -1;
        Parent = null;
    }

}
}
                                        
                                    

Pushables

We are going for a Faster Than Light ripoff mixed with Overcooked. So our inventory system should be object-based in our ship. To create chaos for the players.
The server detects when a player input is being blocked by a pushable, if that pushable has space in the same direction as the input, it will be pushed. You can also drag pushables.
As with the pathfinding, the clients only the position to lerp to foreach moveable object in the scene. All checks for pushables are done on the server from the movement input provided by the Client player.

                                        
using UnityEngine;
using System.Collections.Generic;

[System.Serializable]
public class ServerInteractorManager
{
    [SerializeField] private Grid _grid;
    public Dictionary _interactables = new Dictionary();

    public bool LookForInteractable(Vector2Int checkLocation)
    {
        if (_interactables.ContainsKey(checkLocation))
            return true;
        return false;
    }

    public void AddItem(Vector2Int position, InteractData interactData)
    {
        if (LookForInteractable(position))
            return;

        _interactables.Add(position, interactData);
        _grid.OccupyCell(position, true);
        Server.Send(new InteractablesMessage{MoveTo = position, InteractData = interactData});
    }

    public bool RemoveItem(Vector2Int checkLocation)
    {
        return _interactables.Remove(checkLocation);
    }

    public void MoveInteractable(Vector2Int checkLocation, Vector2Int input)
    {
        _grid.OccupyCell(checkLocation, false);
        _grid.OccupyCell(checkLocation + input, true);

        InteractData temp = _interactables[checkLocation];
        temp.Position = checkLocation + input;
        _interactables.Add(temp.Position,  temp);
        _interactables.Remove(checkLocation);

        Server.Send(new InteractablesMessage{MoveTo = input, InteractData = temp, CheckLocation = checkLocation});
    }
}
                                        
                                    


                                        
public class ProceduralAnimeController : MonoBehaviour
{
    [SerializeField] private Transform _target;

    [Header("Head")] [SerializeField] private Transform _headBone;
    [SerializeField] private float _headMaxTurnAngle;
    [SerializeField] private float _headTrackingSpeed;

    [Header("Eyes")] [SerializeField] private Transform _leftEyeBone;
    [SerializeField] private Transform _rightEyeBone;
    [SerializeField] private float _eyeTrackingSpeed;
    [SerializeField] private float _leftEyeMaxYRotation;
    [SerializeField] private float _leftEyeMinYRotation;
    [SerializeField] private float _rightEyeMaxYRotation;
    [SerializeField] private float _rightEyeMinYRotation;

    [Header("Legs")]
    [SerializeField] private LegIKStepper _frontLeftLegStepper;
    [SerializeField] private LegIKStepper _frontRightLegStepper;
    [SerializeField] private LegIKStepper _backLeftLegStepper;
    [SerializeField] private LegIKStepper _backRightLegStepper;

    [Header("Movement")]
    [SerializeField] private float _turnSpeed;
    [SerializeField] private float _moveSpeed;
    [SerializeField] private float _turnAcceleration;
    [SerializeField] private float _moveAcceleration;
    [SerializeField] private float _minDistToTarget;
    [SerializeField] private float _maxDistToTarget;
    [SerializeField] private float _maxAngelToTarget;

    public Vector3 _currentVelocity;
    public Vector3 FlockAdditon;
    private float _currentAngularVelocity;

    private void Start()
    {    //Set our Stepper to world position. Did this so we can save the whole object as a prefab.
        _frontLeftLegStepper.gameObject.transform.parent = null;
        _frontRightLegStepper.gameObject.transform.parent = null;
        _backLeftLegStepper.gameObject.transform.parent = null;
        _backRightLegStepper.gameObject.transform.parent = null;
        StartCoroutine(LegUpdate());
    }


    void RootMotionUpdate(float dt)
    {
        //Rotation
        Vector3 towardTarget = _target.position - transform.position;
        Vector3 towardTargetProjected = Vector3.ProjectOnPlane(towardTarget, transform.up);
        float angelToTarget = Vector3.SignedAngle(transform.forward, towardTargetProjected, transform.up);
        float targetAngularVelocity = 0;

        if (Mathf.Abs(angelToTarget) > _maxAngelToTarget)
        {
            if (angelToTarget > 0)
                targetAngularVelocity = _turnSpeed;
            else
                targetAngularVelocity = -_turnSpeed;
        }

        _currentAngularVelocity = Mathf.Lerp(_currentAngularVelocity, targetAngularVelocity,
            1 - Mathf.Exp(-_turnAcceleration * Time.deltaTime));

        transform.Rotate(0, _currentAngularVelocity * dt, 0, Space.World); //Y in worldspace

        //Position
        Vector3 targetVelocity = Vector3.zero;

        if (Mathf.Abs(angelToTarget) < 90)
        {
            float distToTarget = Vector3.Distance(transform.position, _target.position);

            if (distToTarget > _maxDistToTarget)
                targetVelocity = _moveSpeed * towardTargetProjected.normalized;

            else if (distToTarget < _minDistToTarget)
                targetVelocity = (_moveSpeed / 2f) * -towardTargetProjected.normalized;

        }

        _currentVelocity = Vector3.Lerp(_currentVelocity, targetVelocity, 1 - Mathf.Exp(-_moveAcceleration * dt));
        transform.position += _currentVelocity * dt;

        if((transform.position - _target.position).magnitude > 5f)
            transform.position += FlockAdditon * dt;

    }

    private void LateUpdate()
    {
        float dt = Time.deltaTime;

        RootMotionUpdate(dt);
        HeadTrackingUpdate(dt);
        EyeTrackingUpdate(dt);
    }

    IEnumerator LegUpdate()
    {
        while (true)
        {
            do
            {
                _frontLeftLegStepper.TryMove();
                _backRightLegStepper.TryMove();
                yield return null;

            } while (_frontLeftLegStepper.Moving || _backRightLegStepper.Moving);

            do
            {
                _frontRightLegStepper.TryMove();
                _backLeftLegStepper.TryMove();
                yield return null;
            } while (_frontRightLegStepper.Moving || _backLeftLegStepper.Moving);
        }
    }
}
                                        
                                    



Work Experience

Stage/Sound Technician - Stockholm City (2015 Q2 - Present)

Jack of all traits kind of job

Store Manager/Salesmen - Kjell & Company (2017 Q1 - 2018 Q3)

Logistics, Salesmen and summer store manager handling inventory, market analysis and job schedules

Logistics Worker - Lundagrossisten (2014 Q3 - 2015 Q3)

Handling incoming/outgoing orders, cleaning and sorting/optimizing our storage facility. "Selfgoing"

Fast Food Worker - Max Burgers (2017 Q2 - 2017 Q4)