Orfeas Eleftheriou https://www.orfeasel.com Sat, 31 Aug 2024 06:23:09 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://i0.wp.com/www.orfeasel.com/wp-content/uploads/2016/03/cropped-favicon_4th.png?fit=32%2C32&ssl=1 Orfeas Eleftheriou https://www.orfeasel.com 32 32 92830608 Resolving Crashes on Shipping Builds https://www.orfeasel.com/resolving-crashes-on-shipping-builds/?utm_source=rss&utm_medium=rss&utm_campaign=resolving-crashes-on-shipping-builds https://www.orfeasel.com/resolving-crashes-on-shipping-builds/#comments_reply Sat, 31 Aug 2024 06:22:00 +0000 https://www.orfeasel.com/?p=45477 While our game may be running without any issues in the editor or even in development builds, sometimes we need to resolve crashes that may take place in shipping configurations only. In this post I’m going to demonstrate an easy workflow that you can use in order to debug your shipping builds. Requirements before debugging…

The post Resolving Crashes on Shipping Builds first appeared on Orfeas Eleftheriou.]]>
While our game may be running without any issues in the editor or even in development builds, sometimes we need to resolve crashes that may take place in shipping configurations only. In this post I’m going to demonstrate an easy workflow that you can use in order to debug your shipping builds.

Requirements before debugging the crash:

  • Editor Symbols for Debugging. You can download these from the options menu in the Epic Games Launcher for the specific version of Unreal you’re using.
  • The .exe file that produces the crash of your build
  • The .dmp file of the crash. This is usually located in C:/Users/<YourUserName>/AppData/Local/<NameOfYourGame>/Saved/Crashes
  • A .pdb file (program debug database). These files are generated via Unreal every time you build your game.

It is important to note that the .pdb file you are using must match the actual build of the .exe file otherwise you will get false information as your IDE won’t be able to match your crash with the actual source code block that raises it. In other words, if your game has multiple versions and you need to resolve a crash in an older version you need to recompile your game in the same, older version. By keeping an organized source control process you should have access to the required older version of the source code! :)

I’m going to start off by showing how to do this in Rider and then move on to Visual Studio. In case you’re using VS feel free to entirely skip the Rider part (and vice versa!)

Rider Workflow

To debug your crash using rider follow the steps below:

  • Go to Run / Debug configurations and select Edit Configurations
  • In the menu that appears add a new configuration and select Native Core Dump Debug
  • In the Core Dump Debug window, configure the following properties:
    • Name (The name of your configuration – Can be named after the bug you’re currently looking for)
    • In the Core Dump Path set the .dmp file which was generated by the crash
    • In the Exe Path select the .exe file that raised the crash
    • In the Binaries search paths, add a new path and select the folder which contains the .pdb file for this particular build
Configuration menu in Rider
Setting up the core dump path file, exe path and binaries path

Once you set everything up, press alt+f5 or click debug in Rider and Rider will let you know about where the crash happened:

Now of course, line 802 is intended to crash the game in order for me to make this blog post! :)

Visual Studio workflow

To repeat the same process as above:

  • Open your Visual Studio without opening any project
  • Go to File->Open->File and open up your .dmp file
  • Once VS loads up the file:
    • Set the symbols path to match your project’s binaries
    • Click “Debug with Native Only”
  • Then, Visual Studio will point us to the exact same location that raised the crash, as did Rider :)
Visual Studio informing us about where the crash happened!
The post Resolving Crashes on Shipping Builds first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/resolving-crashes-on-shipping-builds/feed/ 0 45477
Westwood Shadows Dev Log https://www.orfeasel.com/westwood-shadows-dev-log/?utm_source=rss&utm_medium=rss&utm_campaign=westwood-shadows-dev-log https://www.orfeasel.com/westwood-shadows-dev-log/#comments_reply Tue, 01 Nov 2022 07:32:50 +0000 https://www.orfeasel.com/?p=13192 About a year ago Eleftherios Kokkinakis, the owner of RedSoup Studios which was developing Westwood Shadows, a first-person puzzle solving game contacted me about the development of the AI for the game. In Westwood Shadows, you’re Peter Bennet, a police detective returning to his abandoned hometown where he confronts his past unsolved case.  Westwood Shadows…

The post Westwood Shadows Dev Log first appeared on Orfeas Eleftheriou.]]>
About a year ago Eleftherios Kokkinakis, the owner of RedSoup Studios which was developing Westwood Shadows, a first-person puzzle solving game contacted me about the development of the AI for the game. In Westwood Shadows, you’re Peter Bennet, a police detective returning to his abandoned hometown where he confronts his past unsolved case.

 Westwood Shadows is currently available as Early Access on Steam:

Initial AI Design

Initially, we designed a base enemy type, codenamed as ‘Shadow’, which would bring some mild horror elements to the game and challenge Peter. The original design was Shadows would patrol around levels and reduce Peter’s sanity based on their distance and line of sight. Originally, when Shadows would catch the player, a quick time event (QTE) would kick off where Peter had to struggle in order to release himself from the effects of the shadow which was taking a hold of him. Once QTE was finished, Shadows would refrain from chasing Peter for a brief period of time on order to give the opportunity to the player to run away. After thoroughly testing the system, we felt like it didn’t mix well with the overall game so we decided to disable it.

Additionally, we designed and implemented some hearing functionality on Shadows. Once a sound has occurred in the game, whether this is a loud footstep close to a shadow, or a gunshot noise, or a throwable item which was tossed next to an enemy, Shadows would start investigating around the source of noise. This resulted in a smarter AI that didn’t solely rely on seeing the player in order to start chasing Peter. Last but not least, we implemented a system that activated investigation of fixed points around levels in case Shadows would chase the player but couldn’t reach him for whatever reason. The result of this system was that Shadows would start investigating around the last known player location during the chase which gave the impression that Shadows are intelligent.

Boss Fight Design

Besides the base enemy type, we also designed and implemented four boss fights, however one of them was scratched during the development as it didn’t quite fit the game overall.

First boss – Denial Shadow

The first boss that Peter will face in the game (internally named as Denial Shadow) will chase him inside a maze. Peter has to overcome various physical obstacles along the way and find the correct escape path. The Denial Shadow is immune to damage and effectively “oneshots” Peter once it catches him. Last but not least, to give the player a chance to take a breather we implemented some safe areas where the boss would disappear so the player could take a short rest.

Second Boss – Anger Shadow

The second boss that players will face is codenamed “Anger Shadow”. It’s essentially the Denial Shadow with two major differences: this time the boss doesn’t kill you immediately while Peter has to kill it in combat. The boss fight is made up from different phases:

  • On the first phase, the Anger Shadow will go close to Peter and transform into a “Shadow Orb” which will try to reach the player. Once it reaches the player the orb will explode, dealing damage to Peter. Players can run away from the orb in order to avoid any damage.
  • On the second phase, the Shadow gains an additional skill, called Shadow Area. The Shadow detects if the player is standing still for a certain amount of time and it spawns a shadow area below the player’s feet. This area slows down the player but it can be killed using the “Strobo Light” item that the player has picked up at the start of the game.
  • On the third and final phase, the Shadow gains another ability, called “Shadow Walls”. After certain intervals, the boss blocks the path of player around the level, forcing the player to either stand still or having to deal with the Shadow Orb attack.

Third Boss – Depression Shadow

The third and final boss of the game is internally named as Depression Shadow. This Shadow is immune to any damage and will deal periodical damage to the player based on proximity. The boss fight is effectively a time trial fight as it takes place in a level that Peter has to complete an objective in order for the fight to stop.

Conclusion

Overall, I had a blast working on this game and while it didn’t push my technical limits on the programming part, I learnt a lot of things about game design. Additionally, I gained invaluable experience when it comes to preparing tools and systems that were used by the designers of the game.

Thank you for making it so far on yet another post and make sure to check out Westwood Shadows by purchasing the Early Access or by playing its free Demo!

The post Westwood Shadows Dev Log first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/westwood-shadows-dev-log/feed/ 0 13192
Tangled Orbs Postmortem: An overview of my latest solo game project https://www.orfeasel.com/tangled-orbs-postmortem-an-overview-of-my-latest-solo-game-project/?utm_source=rss&utm_medium=rss&utm_campaign=tangled-orbs-postmortem-an-overview-of-my-latest-solo-game-project https://www.orfeasel.com/tangled-orbs-postmortem-an-overview-of-my-latest-solo-game-project/#comments_reply Tue, 08 Feb 2022 09:41:06 +0000 https://www.orfeasel.com/?p=6062 How the game came to be During last year’s summer a good friend of mine was working night shifts in a restaurant. Right at the beginning of his shift he was tasked with setting up the tables for the diners to come. Once he was done, he had a downtime in which he was mostly…

The post Tangled Orbs Postmortem: An overview of my latest solo game project first appeared on Orfeas Eleftheriou.]]>
How the game came to be

During last year’s summer a good friend of mine was working night shifts in a restaurant. Right at the beginning of his shift he was tasked with setting up the tables for the diners to come. Once he was done, he had a downtime in which he was mostly playing some casual games on his phone. After we had a few conversations about casual games, I decided to create my own casual game for Android devices.

To start things off I researched a few different genres of casual games until I realized that I wanted to build a puzzle / logic game. After a couple of days, I recalled the WoW quest, Leylocked chest, where you had to untangle some orbs in order to get the quest reward. Since I really enjoyed solving that puzzle, I decided to create a similar game with lots of different scenarios.

Once I had the design of the game cut out, I started working on my game which would end up be Tangled Orbs which is currently available on Google Play!

Get it on Google Play

What went right

Since the scope of the game was small, the mechanics were straightforward to build. Each different level, internally called scenario, was made up from different orbs and their individual connections. The scenario was considered solved if all different connections had zero overlaps. Since generating lots of levels by hand takes time, I created some tools that really accelerated the process. Most notably I created the following systems:

  • A python script that was generating a certain number of orbs directly inside the Editor in random locations. The script was really flexible as it included rules such as minimum distance between spawned orbs (to avoid overlaps) or maximum extents of the spawn locations.
  • A python script that was generating connections between spawned orbs.
  • A plugin that allowed orb switching during Editor mode. This plugin really helped in identifying the solution for each scenario without having to fire up the game as you could solve the scenario in editor time.
  • A script which was responsible for exporting the editor scenario into a json file.

By using all of the plugins and tools I was able to generate all 50 different scenarios for the game in a couple of days without any hassle.

What went wrong

The main thing that went wrong with this project was deciding the art direction that I wanted to go with. Even if the game was consisted of primitive shapes (the orbs are actually spheres) it was really hard for me to have something “presentable”. After spending weeks trying to create something by myself, I reached out to Hotgates, a friend of mine who was really keen on helping me get out of this dire situation. After Evan jumped in the project the overall look of the project took off in a matter of days (in case you’re interested in hiring him check out his work on the UE4 marketplace and his website).

Comparison screenshot of the same scenario

Another thing that went wrong was the overall UX of the game. The initial implementation of the user interface was really unintuitive and somewhat clunky for a simple game like this. In the end, I completely redesigned the game’s UI which ultimately took a few days.

Conclusion

I really enjoyed creating this game as it provided the opportunity to work on something different. I learned some new tricks, most notably with content generation that can help with future projects. Additionally, I gained some experience with dealing with issues in Android devices.

Thanks for reading this short postmortem and have an excellent day!

The post Tangled Orbs Postmortem: An overview of my latest solo game project first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/tangled-orbs-postmortem-an-overview-of-my-latest-solo-game-project/feed/ 0 6062
Creating Latent Blueprint Nodes with Multiple Execution Pins https://www.orfeasel.com/creating-latent-blueprint-nodes-with-multiple-execution-pins/?utm_source=rss&utm_medium=rss&utm_campaign=creating-latent-blueprint-nodes-with-multiple-execution-pins https://www.orfeasel.com/creating-latent-blueprint-nodes-with-multiple-execution-pins/#view_comments Mon, 19 Apr 2021 12:21:18 +0000 https://www.orfeasel.com/?p=4541 In this post I’m going to show you how to create latent Blueprint nodes that have multiple execution pins inside the Editor! The code in this post is written in version 4.26 so depending on your version you may need to perform a few changes. Creating a latent node with multiple output pins In this…

The post Creating Latent Blueprint Nodes with Multiple Execution Pins first appeared on Orfeas Eleftheriou.]]>
In this post I’m going to show you how to create latent Blueprint nodes that have multiple execution pins inside the Editor! The code in this post is written in version 4.26 so depending on your version you may need to perform a few changes.

Creating a latent node with multiple output pins

In this section, we’re going to create a latent node which will tick a few times depending on our input. For demonstration purposes, we’re going to create a node that will calculate a few fibonacci numbers in each tick.

Fortunately, it’s quite easy to create nodes with multiple output pins. To do that, create a new C++ class that inherits the BlueprintAsyncActionBase and add the following code to its header file:

/**
 * Signatures of execution pins in the editor
 */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FBPNodeOutputPin,int32,FibonacciNumber);

/**
 * 
 */
UCLASS()
class LATENTNODES_API UBPAsyncActionBase : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
	
private:

	/**
	 * Keeps track of the last sequence number
	 * Used in order to stop the execution at some point
	 */
	int32 FiboNum;

	/**
	 * Contains the latest result from FibonacciNum
	 */
	int32 LastResult;

	/**
	 * Recursive way of computing a fibonacci number
	 * @param N the number of the sequence https://en.wikipedia.org/wiki/Fibonacci_number
	 */
	int32 FibonacciNum(int32 N);

	/**
	 * Internal tick. Will also broadcast the Tick Execution pin in the editor
	 */
	UFUNCTION()
	void InternalTick();

	/**
	 * Internal completed. Clears timers and flags and broadcasts Completed Execution pin in the editor
	 */
	UFUNCTION()
	void InternalCompleted();

	/**
	 * Static property to prevent restarting the async node multiple times before execution has finished
	 */
	static bool bActive;

	/**
	 * Numbers of fibonacci numbers to calculate
	 */
	int32 NumsToCalculate;

	/**
	 * World context object to grab a reference of the world 
	 */
	const UObject* WorldContext;

	/**
	 * Timer handle of internal tick
	 */
	FTimerHandle TimerHandle;

public:

	UPROPERTY(BlueprintAssignable)
	FBPNodeOutputPin Tick;

	UPROPERTY(BlueprintAssignable)
	FBPNodeOutputPin Completed;

	/**
	 * InternalUseOnly to hide sync version in BPs
	 */
	UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject"), Category = "AsyncNode")
	static UBPAsyncActionBase* BPAsyncNode(const UObject* WorldContextObj, int32 Num);

	//Overriding BP async action base
	virtual void Activate() override;
};

Then, in the source file, add the following code:

bool UBPAsyncActionBase::bActive=false; //Init bactive for all instances

int32 UBPAsyncActionBase::FibonacciNum(int32 N)
{
	if (N == 0)
	{
		return 0;
	}
	else if (N == 1)
	{
		return 1;
	}
	else
	{
		return FibonacciNum(N-1) + FibonacciNum(N-2);
	}
}

void UBPAsyncActionBase::InternalTick()
{
	FiboNum++;
	LastResult = FibonacciNum(FiboNum);
	Tick.Broadcast(LastResult);
	
	if (FiboNum >= NumsToCalculate)
	{
		InternalCompleted();
	}
}

void UBPAsyncActionBase::InternalCompleted()
{
	if (WorldContext)
	{
		WorldContext->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
		TimerHandle.Invalidate();
		Completed.Broadcast(LastResult);
		UBPAsyncActionBase::bActive=false;
	}
}

UBPAsyncActionBase* UBPAsyncActionBase::BPAsyncNode(const UObject* WorldContextObj, int32 Num)
{
	UBPAsyncActionBase* Node = NewObject&lt;UBPAsyncActionBase>();
	if (Node)
	{
		Node->WorldContext = WorldContextObj;
		Node->NumsToCalculate=Num;
	}
	return Node;
}

void UBPAsyncActionBase::Activate()
{
	if (UBPAsyncActionBase::bActive)
	{
		FFrame::KismetExecutionMessage(TEXT("Async action is already running"), ELogVerbosity::Warning);
		return;
	}

	FFrame::KismetExecutionMessage(TEXT("Started Activate!"), ELogVerbosity::Log);

	if (WorldContext)
	{
		UBPAsyncActionBase::bActive=true;
		FTimerDelegate TimerDelegate;
		TimerDelegate.BindUObject(this,&amp;UBPAsyncActionBase::InternalTick);
		WorldContext->GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDelegate, 0.1f, true);
	}
	else
	{
		FFrame::KismetExecutionMessage(TEXT("Invalid world context obj"), ELogVerbosity::Error);
	}

}


Compile your code and search for BPAsync Node somewhere in your blueprint graph:

(Click on image to enlarge)

If the node doesn’t appear, restart the editor.

Creating a latent node with multiple input pins

To create a latent node with multiple inputs we need the following:

  • A way to specify the input pins
  • The logic of what’s going to happen once each pin has fired

To demonstrate this kind of node, we’re going to create a function that interpolates an actor between two locations simulating a ping pong effect.

From the engine’s class wizard, create a new C++ class which doesn’t inherit anything and add the following code:

#include "CoreMinimal.h"
#include "UObject/WeakObjectPtr.h"
#include "GameFramework/Actor.h"
#include "Engine/LatentActionManager.h"
#include "Engine/Public/LatentActions.h"

/**
 * PingPongAction is the class which will handle the interpolation of an Actor between two locations
 */
class LATENTNODES_API FPingPongAction : public FPendingLatentAction
{
public:

	
	/** Actor we're going to interpolate */
	AActor* ActorToPingPong;

	/** Starting location of interpolation */
	FVector InitialLocation;
	
	/** Target location of interpolation */
	FVector TargetLocation;

	/** True when we're stopping the movement */
	bool bComplete;

	/** Elapsed time of a single interpolation */
	float TimeElapsed;

	/** Total time for the interpolation from Initial to Target location*/
	float TotalTime;

	/** Function to execute on completion */
	FName ExecutionFunction;

	/** Link to fire on completion */
	int32 OutputLink;

	/**
	 * Object to call callback on upon completion
	 */
	FWeakObjectPtr CallbackTarget;

	FPingPongAction(AActor* InActor, FVector InitLoc, FVector TargetLoc, const FLatentActionInfo&amp; LatentInfo) 
		: ActorToPingPong(InActor)
		, InitialLocation(InitLoc)
		, TargetLocation(TargetLoc)
		, bComplete(false)
		, TimeElapsed(0.f)
		, TotalTime(1.f)
		, ExecutionFunction(LatentInfo.ExecutionFunction)
		, OutputLink(LatentInfo.Linkage)
		, CallbackTarget(LatentInfo.CallbackTarget)
	{
	}

	virtual void UpdateOperation(FLatentResponse&amp; Response) override
	{
		TimeElapsed+=Response.ElapsedTime();
		float Alpha = TimeElapsed / TotalTime;

		if (ActorToPingPong)
		{
			
			FVector NewLocation = FMath::Lerp(InitialLocation,TargetLocation,Alpha);

			ActorToPingPong->SetActorLocation(NewLocation);
			//If we have reached the target location swap initial and target
			//Hardcoded tolernace just for demonstration purposes.
			if (NewLocation.Equals(TargetLocation,15.f))
			{	
				FVector TempLocation = TargetLocation;
				TargetLocation = InitialLocation;
				InitialLocation = TempLocation;
				TimeElapsed=0.f; //Restart timer as well
			}

		}

		Response.FinishAndTriggerIf(bComplete || Alpha>=1.f,ExecutionFunction,OutputLink,CallbackTarget);
	}

#if WITH_EDITOR
	// Returns a human readable description of the latent operation's current state
	virtual FString GetDescription() const override
	{
		static const FNumberFormattingOptions DelayTimeFormatOptions = FNumberFormattingOptions()
			.SetMinimumFractionalDigits(3)
			.SetMaximumFractionalDigits(3);
		return FText::Format(NSLOCTEXT("FPingPongAction", "ActionTimeFmt", "Ping Pong ({0} seconds left)"), FText::AsNumber(TotalTime - TimeElapsed, &amp;DelayTimeFormatOptions)).ToString();
	}
#endif
};

Once you’re done with this code, add a new Blueprint Function Library class in your project and type in the following code:

UENUM()
namespace EPingPongStatus
{
	enum Type
	{
		Start,
		Stop
	};
}

/**
 * 
 */
UCLASS()
class LATENTNODES_API UMyBPFunctionLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
	
public:

	/**
	 * Interpolates the given actor between the given locations
	 * @param InActor - the actor to interpolate
	 * @param LocA - the starting location of the interpolation
	 * @param LocB - the target location of the interpolation
	 * @param PingPongStatus - the status of the ping pong
	 * @param LatentInfo - the latent info to handle the progress in the background
	 */
	UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", WorldContext = "WorldContextObject", ExpandEnumAsExecs = "PingPongStatus"), Category = "MyBPFunctions")
	static void PingPong(AActor* InActor, FVector LocA, FVector LocB, TEnumAsByte&lt;EPingPongStatus::Type> PingPongStatus, FLatentActionInfo LatentInfo);

};

Then, inside the source file add the following code:

#include "MyBPFunctionLibrary.h"
#include "GameFramework/Actor.h"
#include "Engine/Engine.h"
#include "Engine/LatentActionManager.h"
#include "PingPongAction.h"


void UMyBPFunctionLibrary::PingPong(AActor* InActor, FVector LocA, FVector LocB, TEnumAsByte&lt;EPingPongStatus::Type> PingPongStatus, FLatentActionInfo LatentInfo)
{
	if (UWorld* World = GEngine->GetWorldFromContextObjectChecked(InActor))
	{
		FLatentActionManager&amp; LatentActionManager = World->GetLatentActionManager();

		FPingPongAction* PingPongAction = LatentActionManager.FindExistingAction&lt;FPingPongAction>(LatentInfo.CallbackTarget,LatentInfo.UUID);
		
		//If not currently running
		if (!PingPongAction)
		{
			if (PingPongStatus == EPingPongStatus::Start)
			{
				PingPongAction = new FPingPongAction(InActor,LocA,LocB, LatentInfo);

				LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, PingPongAction);
			}
		}
		else
		{
			if (PingPongStatus == EPingPongStatus::Stop)
			{
				PingPongAction->bComplete=true;
			}
		}
	}
}

At this point, you can compile your code and test the ping pong action we have created! You can open up any blueprint graph and search for the PingPong node (if the node doesn’t appear, you may need to restart your editor).

To sum up, in order to create a node with multiple input execution pins we created:

  • An “Action” class which is performing the logic of what’s happening when each pin fires and
  • An Enum which allows us to expose the different options in the UE Editor
Ping Pong node

The post Creating Latent Blueprint Nodes with Multiple Execution Pins first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/creating-latent-blueprint-nodes-with-multiple-execution-pins/feed/ 1 4541
Creating Functional Tests with the Automation System https://www.orfeasel.com/functional-tests/?utm_source=rss&utm_medium=rss&utm_campaign=functional-tests https://www.orfeasel.com/functional-tests/#comments_reply Wed, 07 Apr 2021 08:19:33 +0000 https://www.orfeasel.com/?p=4480 In this post I’m going to show you how to create Functional Tests with the Automation System that comes with the latest version of Unreal Engine 4. With functional tests, we can simulate a user’s behavior (to some extent) and verify that everything is working properly. In this post we’re going to create a simple…

The post Creating Functional Tests with the Automation System first appeared on Orfeas Eleftheriou.]]>
In this post I’m going to show you how to create Functional Tests with the Automation System that comes with the latest version of Unreal Engine 4. With functional tests, we can simulate a user’s behavior (to some extent) and verify that everything is working properly.

In this post we’re going to create a simple script that tests if the player can move X units forward to a level. Once you become familiar with the process you will be able to extend the testing functionality to interactions or more complex systems of your game.

Implementing a functional test

Fortunately, Unreal provides a straightforward to jump start our functional tests. For this post, I’m using a C++ Third Person Template project. To create a functional test, add a new C++ class that inherits the FunctionalTest class. In your header file, add the following code:

#include "CoreMinimal.h"
#include "FunctionalTest.h"
#include "PlayerMovementFunctionalTest.generated.h"

/**
 * A functional test that moves the player character forward
 */
UCLASS()
class AUTOMATIONPOST_API APlayerMovementFunctionalTest : public AFunctionalTest
{
	GENERATED_BODY()

private:

	/* Reference of main player */
	class ACharacter* Player;

	/* Initial location of the player */
	FVector InitialLocation;

protected:

	virtual void BeginPlay() override;

	/* Total movement distance */
	UPROPERTY(EditAnywhere)
	float MovementDistance = 250.f;

	/* Distance threshold in order to get for target location */
	UPROPERTY(EditAnywhere)
	float DistanceThreshold=55.f;

public:

	virtual void Tick(float DeltaSeconds) override;

	/**
	 * Returns true when the player has traveled MovementDistance - DistanceThreshold
	 */
	bool TraveledTotalDistance() const;
	
};

On the source file, add the following code:

#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"

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


	Player = UGameplayStatics::GetPlayerCharacter(GetWorld(),0);

	if (Player)
	{
		InitialLocation = Player->GetActorLocation();
	}
}

void APlayerMovementFunctionalTest::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	if (Player)
	{
		//Go forward until we reach our destination
		Player->AddMovementInput(Player->GetActorForwardVector());
		if (TraveledTotalDistance())
		{
			FinishTest(EFunctionalTestResult::Succeeded,FString("Traveled required units!"));
			UKismetSystemLibrary::QuitGame(GetWorld(),UGameplayStatics::GetPlayerController(GetWorld(),0),EQuitPreference::Quit,false);
		}
	}
	else
	{
		FinishTest(EFunctionalTestResult::Failed, FString("Invalid player character!"));
	}
}

bool APlayerMovementFunctionalTest::TraveledTotalDistance() const
{
	return (Player) ? (FVector::Distance(Player->GetActorLocation(), InitialLocation + Player->GetActorForwardVector() * MovementDistance) &lt;= DistanceThreshold) : false;
}

Compile your code and navigate to your Editor and locate your C++ class. If the C++ Classes folder isn’t visible, make sure to Activate the Show C++ filter from the editor

(Click on image to enlarge)

Once you locate your C++ class, just drag and drop it into your level:

(Click on image to enlarge)

At this point, open up the Session FrontEnd window and notice that in the Automation Tab a new test was added under the Project Category:

(Click on image to enlarge)

In order to run your test you can either:

  • Enable its checkbox and press the Start Tests or
  • Press the Run Level Test. The editor has already marked this level as a test once you placed a functional test in the world
Functional Test

And this is how you can create a simple functional test for your game! Thanks for reading and have a nice day!

The post Creating Functional Tests with the Automation System first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/functional-tests/feed/ 0 4480
Creating Unit Tests with the Automation System https://www.orfeasel.com/unit-testing/?utm_source=rss&utm_medium=rss&utm_campaign=unit-testing https://www.orfeasel.com/unit-testing/#comments_reply Mon, 05 Apr 2021 13:00:15 +0000 https://www.orfeasel.com/?p=4459 In this post I’m going to show you how to use Unreal’s Automation System in order to create unit tests for your code. Unit tests are tests written by developers in order to test various aspects of their code and identify bugs or other unintended behavior before it happens. In order to test out the…

The post Creating Unit Tests with the Automation System first appeared on Orfeas Eleftheriou.]]>
In this post I’m going to show you how to use Unreal’s Automation System in order to create unit tests for your code. Unit tests are tests written by developers in order to test various aspects of their code and identify bugs or other unintended behavior before it happens.

In order to test out the Automation System, open up the .Build.cs file of your project and add “UnrealEd” and “Engine” on the dependecy modules list:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "UnrealEd", "Engine", "InputCore", "HeadMountedDisplay" });

Implementing a dummy class

For the sake of this post, I’m also adding a dummy class to test its behavior:

struct FMathStruct
{
	static int32 Add(int32 A, int32 B);

	static float Add(float A, float B);
};

int32 FMathStruct::Add(int32 A, int32 B)
{
	return A+B;
}

float FMathStruct::Add(float A, float B)
{
	return A+B;
}

Creating the first unit test

We have created a class that we can test so go ahead and create a new class. On its source file add the following code:

#include "Tests/MathTests.h"
#include "AutomationPost/TestClass.h" //Contains FMathStruct
#include "Misc/AutomationTest.h"


#if WITH_DEV_AUTOMATION_TESTS

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMathStructTest, "MathTests",EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::SmokeFilter)


bool FMathStructTest::RunTest(const FString&amp; Parameters)
{
	//Addition tests
	{
		int32 ResultA = FMathStruct::Add(5,15);
		int32 ExpectedResultA = 20;
		float ResultB = FMathStruct::Add(3.5f,1.5f);
		float ExpectedResultB = 5.f;
		
		TestEqual(TEXT("Testing sum in integers"), ResultA, ExpectedResultA);
		TestEqual(TEXT("Testing sum in floats"),ResultB, ExpectedResultB);
	}

	return true;
}


#endif //WITH_DEV_AUTOMATION_TESTS

The IMPLEMENT_SIMPLE_AUTOMATION_TEST macro defines the class which contains our test. The macro requires:

  • The name of the class we want to define (in this case FMathStructTest)
  • A string which defines tha category that this test will be assigned in the Editor
  • Some flags regarding our test

For more information about the different flags you can use I would suggest to check out the AutomationTest.h file in the Engine’s source code. In this case, I have added the ApplicationContextMask flag which makes this test availabe in the Editor and the SmokeFilter which means that we expect this test to be completed quickly. In the provided link you can find out more about those flags so I would highly suggest to check it out!

Since we’re happy with the first unit test we have created, you can compile your code and open up the “Session Frontend” window from Window->Developer Tools. When you open up the Session Frontend you can click the Automation tab and you can locate the test we have created above:

(Click on image to enlarge)

If the test doesn’t appear on that window try clicking “Refresh Tests” or restarting your editor. Once you select the MathTests test you can click the Start Tests button and the editor will run your tests:

(Click on image to enlarge)

As we can see the results from the previous test passed. In case the result would fail, the editor would inform us about what went wrong. For example, if we change the expected result in the first addition on the code above from 20 to 19 we’re going to get the following result:

(Click on image to enlarge)

In this case I changed the expected value of the test from 20 to 19 and we can see that the editor informs us about why the test failed. Moreover, if you click on the red mathtests button in the SessionFrontend window the editor will display the error message of the output log to your window.

Creating another unit test

The previous unit test was pretty straightforward, however in real applications we may need to check various stuff inside our levels. For demonstration purposes, I created an automated process that verifies that we have a valid character placed inside the level we have currently open in the Editor:

#include "Tests/LevelTests.h"
#include "Misc/AutomationTest.h"
#include "Tests/AutomationEditorCommon.h"
#include "Editor.h"
#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"
#include "AutomationPost/AutomationPostCharacter.h"

#if WITH_DEV_AUTOMATION_TESTS

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FValidPlayerLevelTest, "TestCategory.LevelRelated.Check for Valid Players", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter)

bool FValidPlayerLevelTest::RunTest(const FString&amp; Parameters)
{
	//Checking if there is at least one player placed in the world
	
	UWorld* World = GEditor->GetWorld();
	{
		if (World)
		{
			ACharacter* PlayerChar = UGameplayStatics::GetPlayerCharacter(World,0);

			//For more tests check AutomationTest.h (lines 1347 - 1639)
			TestNotNull&lt;ACharacter>(TEXT("Testing if there is a player actor in level"),PlayerChar);
		}
	}

	return true;
}

#endif //WITH_DEV_AUTOMATION_TESTS

Once you compile your code the Automation Tab should detect the new test and place it in the corresponding category:

(Click on image to enlarge)

Creating Complex Tests

Sometimes we want to test complex stuff and we don’t know exactly the duration of tests. Fortunately, Unreal provides an easy to use way of creating latent tests that can run for some time, allowing us to test things out. To implement a complex test we need to:

  • Define a new complex test
  • Override the GetTests function
  • Add the various tests in our complex test

Create a new class and in the header file add the following code in order to create a new test:

#include "CoreMinimal.h"
#include "Misc/AutomationTest.h" //For IAutomationLatentCommand

/**
 * A command that gets completed after a random delay
 */
class FRandomDelayCommand : public IAutomationLatentCommand
{

private:

	/* Min delay in seconds */
	float MinDelay;

	/* Max delay in seconds */
	float MaxDelay;

	/* False by default, true when we have started the test */
	bool bTestStarted;

	/* The actual time in seconds we're going to delay */
	float Delay;

	/* Stores the time we have started our test */
	FDateTime StartedTime;

public:

	FRandomDelayCommand(float Min, float Max) : MinDelay(Min), MaxDelay(Max), bTestStarted(false), Delay(0.f) {}

	/**
	 * Will execute each frame and will stop once we return true
	 */
	virtual bool Update() override;
};

On the source file of our class, add the following code:

#include "Tests/ComplexTest.h"
#include "Tests/AutomationEditorCommon.h" //for EngineAutomationTestUtilities
#include "Tests/AutomationCommon.h" //for FWaitLatentCommand or other latent commands
#include "FileHelpers.h"

#if WITH_DEV_AUTOMATION_TESTS

//Defining a complex test class named FComplexAutomationTest
IMPLEMENT_COMPLEX_AUTOMATION_TEST(FComplexAutomationTest,"TestCategory.LevelRelated.A Complex Test",EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter)

//Once we define a complex test, we need to override the GetTests function in order to build various parameters for our Tests. 
//The OutTestCommands will be contained in the Parameters parameter of the RunTest function
//The OutBeautifiedName will be used from the editor in the Automation tab as a name for each test
void FComplexAutomationTest::GetTests(TArray&lt;FString>&amp; OutBeautifiedNames, TArray&lt;FString>&amp; OutTestCommands) const
{
	TArray&lt;FString> FileList;

#if WITH_EDITOR
	FEditorFileUtils::FindAllPackageFiles(FileList);
#else
	// Look directly on disk. Very slow!
	FPackageName::FindPackagesInDirectory(FileList, *FPaths::ProjectContentDir());
#endif

	// Iterate over all files, adding the ones with the map extension
	for (int32 FileIndex = 0; FileIndex &lt; FileList.Num(); FileIndex++)
	{
		const FString&amp; Filename = FileList[FileIndex];

		//Get all files that have a .umap extension
		if (FPaths::GetExtension(Filename, true) == FPackageName::GetMapPackageExtension())
		{
			//OutBeautifiedName contain names such as: ThirdPersonDefaultMap
			//OutTestCommands contain names such as: &lt;CompletePathToYourProject>/ThirdPersonDefaultMap.umap
			OutBeautifiedNames.Add(FPaths::GetBaseFilename(Filename));
			OutTestCommands.Add(Filename);	
		}
	}
}

bool FComplexAutomationTest::RunTest(const FString&amp; Parameters)
{
	//Retrieve the map name
	FString MapName = Parameters;

	//Loads given map name
	FAutomationEditorCommonUtils::LoadMap(MapName);

	//Add a new command to our queue
	ADD_LATENT_AUTOMATION_COMMAND(FRandomDelayCommand(1.f,3.f));

	return true;
}

#endif //WITH_DEV_AUTOMATION_TESTS

bool FRandomDelayCommand::Update()
{
	if (!bTestStarted)
	{
		Delay = FMath::RandRange(MinDelay, MaxDelay);
		GLog->Log("Started update!");
		GLog->Log("Delay value:"+FString::SanitizeFloat(Delay));
		StartedTime=FDateTime::Now();
		bTestStarted = true;
	}
	else if((FDateTime::Now().GetSecond() - StartedTime.GetSecond())>=Delay)
	{
		GLog->Log("finished waiting latent task!");
		GLog->Log("Elapsed time:"+FString::FromInt(FDateTime::Now().GetSecond() - StartedTime.GetSecond()));
		return true;
	}
	return false;
}

Once you compile your code and refresh your tests you will notice a new category and new tests in your Automation Tab:

(Click on image to enlarge)

To sum up, by defining a complex test we’re able to run several different tests on different maps. Once we have defined our tests, we can select the required maps we want to test out and run the tests in each one of them! This post was written using 4.26 version of the engine so depending on your version things may be slightly different.

In the next post, I’m going to show you how to run Functional Tests which simulate a user’s behavior when running our application so stay tuned! Thanks for reading and have a nice day!

The post Creating Unit Tests with the Automation System first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/unit-testing/feed/ 0 4459
Mark One Postmortem: An overview of my first game https://www.orfeasel.com/mark-one-postmortem-an-overview-of-my-first-game/?utm_source=rss&utm_medium=rss&utm_campaign=mark-one-postmortem-an-overview-of-my-first-game https://www.orfeasel.com/mark-one-postmortem-an-overview-of-my-first-game/#view_comments Sun, 07 Mar 2021 20:59:24 +0000 https://www.orfeasel.com/?p=4361 Intro In the past I created several different tech demos in order to get hands on experience with various aspects game development and gain an insight into the different systems that Unreal Engine had to offer. In August 2019, I decided that I wanted create a roguelike shooter game which would ultimately become Mark One.…

The post Mark One Postmortem: An overview of my first game first appeared on Orfeas Eleftheriou.]]>
Intro

In the past I created several different tech demos in order to get hands on experience with various aspects game development and gain an insight into the different systems that Unreal Engine had to offer. In August 2019, I decided that I wanted create a roguelike shooter game which would ultimately become Mark One.

Project overview

When I started my game development journey in 2015, I imagined that at some point in the future I would like to create my own game and publish it on Steam. Since I anticipated that I would be working on this project as a solo developer and considering that I cannot create any art by myself I ended up purchasing some asset packs from Unreal’s marketplace from time to time, whenever I found something that I liked (whether that was complete environments, particles, sounds or 2d art). By doing so, I ended up having a lot of asset packs in my library which would ultimately be included in my game.

At some point near the end of August 2019 I decided that I would start working on my game in September 2019 during my free time since I also had a day job. Originally, I was planning to release the game about 9 months after the start of development (since I was planning the game to be really short in duration).

The main purpose of the project was to gain experience from publishing a complete game that people can play on Steam. In order to identify what exactly I wanted to build, I created a list of all the assets that I had in my library and wrote down several different ideas about how to utilize them. Since the programming part was my strong suit, I created a game design based on the assets that I had in hand.

After this process was over, I ended up designing a roguelike shooter game (to achieve some kind of replayability), however this presented a lot of unanticipated problems which will be listed below.

What went wrong

Let’s start by listing the things that went wrong. After taking a few afternoons to program the basic movement and interaction functionality for the game, I decided the next step would be to create the world generation and the AI functionality.

Initially, for the world generation part, my plan was to create several different “mini – levels” that would connect with each other in order to create the world for the game. This idea has been used in several games (most recent game that comes to mind is Dead Cells) and seems to be working great.

While the implementation of the procedural generation was straight forward, what I didn’t expect was that it would take literally weeks (for me at least) in order to create 6 different “mini – levels” even with game ready assets. To build on top of that, the balancing of the AI was quite difficult since it required too many iterations based on the generated world. To sum up, it was crystal clear that this system wouldn’t work in my case since it would take too much time to make it “right”. Thus, I stepped back and re-designed the game completely by removing the roguelike element and building a story, with a start and finish.

Another thing that went wrong (which is similar to the previous point) was that even if I removed the roguelike element and the world generation it would still take too much time to build all levels by hand (because as it turns out I’m horrible at this), something that I didn’t anticipate in my original planning phase.

To build on things that went wrong, I also encountered several days that I was demotivated to continue working on the project, which resulted in a need to take several brakes during the development here and there.

The combination of these unexpected problems, plus having day job resulted in a huge delay of about 9 months, which was 100% of the original planned development time!! This would be completely unacceptable if this wasn’t a hobby project.

So, to sum up this section:

  • The procedural world generation method I originally chose required too much work in the level design side
  • Procedural world generation made balancing the game quite difficult
  • My level building workflow was too slow
  • Original scale was too large for a solo developed project
  • Due to the game redesign, I spent time building tools and systems that didn’t make it to the final game (such as fog of war, mini level generator and a few other test systems)
  • Random demotivation periods

What went right

When it comes to things that went right, I was able to produce clean and reusable code that allowed me to build additional systems that I hadn’t initially planned. Moreover, I was fortunate enough to have 10 beta testers which provided invaluable insight which helped me identify what was working well and what was problematic during the game’s development. For instance, originally, the camera was hard attached to the player and didn’t allow any user input to slightly modify its camera angle. A tester, suggested to make the camera more dynamic by letting the user slightly offset the camera based on the mouse location on the screen which ultimately had a huge positive impact in the game’s user experience.

Additionally, I had the chance to test out various AI implementations and build several different systems regarding boss fights, which I really enjoyed.

When it comes to the AI in particular, during the early days of development I had to implement a custom EQS system for my game since the built-in system for the engine was a bit unstable and raised crashes that I wasn’t able to pinpoint or reproduce. As it turns out a few people had that problem as well and to be fair, the system in my engine version is marked as experimental. However, the good news is that EQS is no longer marked as experimental in the latest version of the engine! This system can become quite complex but I was fortunate enough to have a straight forward implementation that didn’t cost too much development time.

Last but not least, it seems like my QA was decent, in some ways at least, since only 2 out of 10 testers reported bugs or game crashes.

So, to sum up this section:

  • Clean and reusable code saved me a lot of time in the long run
  • Beta testers played a huge role in identifying areas of the game that needed improvement early on
  • Various AI implementations (which remained unchanged from the first game design) worked out great
  • My QA process seemed decent enough for the game’s scale

Conclusion

At this point I have to congratulate you for making it this far and thank you for reading this post.

Fortunately, even with the complete game redesign things turned out great! I learned a great deal about a lot of different aspects of game development while being able to focus on the programming side of things. Here is a list of my conclusions from my experience with my project:

  • My original game design was too large
  • Procedural world generation was a terrible idea for my case
  • I’m terrible at building levels, even with game ready assets
  • Splitting the game’s code into small, independent parts allows you to build complex systems unexpectedly fast
  • When demotivation occurs, just open up your project and make at least a single change, even if it’s the background color of the main menu. By performing this sometimes you tend to clock in longer hours than originally expected (which gives you the opportunity to do or at least test more stuff). Even if you don’t end clocking longer hours, at least by the end of the day you’re not on the same spot you were yesterday so this is progress!
  • Take screenshots or videos during the development phase. You will get blown away from your progress when you’re constantly working on the same thing after a period of time
  • If the project is too large, just downscale. Rinse and repeat until the project is doable and don’t give up.
The post Mark One Postmortem: An overview of my first game first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/mark-one-postmortem-an-overview-of-my-first-game/feed/ 1 4361
Mark One Dev Log #3 – Overview of the Animation System https://www.orfeasel.com/mark-one-dev-log-3-overview-of-the-animation-system/?utm_source=rss&utm_medium=rss&utm_campaign=mark-one-dev-log-3-overview-of-the-animation-system https://www.orfeasel.com/mark-one-dev-log-3-overview-of-the-animation-system/#comments_reply Thu, 29 Oct 2020 13:28:12 +0000 https://www.orfeasel.com/?p=4058 Welcome to the third dev log for my game! In this post I’ll provide a brief overview of the animation system running for each character.Before I dive into more details about it in case you want to support my project you can wishlist on Steam using the following link: Need of different animations In Mark…

The post Mark One Dev Log #3 – Overview of the Animation System first appeared on Orfeas Eleftheriou.]]>
Welcome to the third dev log for my game! In this post I’ll provide a brief overview of the animation system running for each character.Before I dive into more details about it in case you want to support my project you can wishlist on Steam using the following link:

Need of different animations

In Mark One all characters are using several different animations at the any given moment. For example, if the player is running he may decide to throw a grenade or dash or he may get hit from an enemy. This presents the need to blend two or more animations together in order to provide:

  • A more realistic experience
  • Visual feedback to the player of what’s happening at any given moment

Player’s character state machine

In order for everything to work together I’m using a single animation blueprint with one state machine for the player. Moreover, the same animation blueprint is used by several enemies in the game since most of their animations are the same and the have the same skeleton as the player’s character:

Click to enlarge on a new tab

Initially, the player’s mesh is playing the Idle animation. From that point onwards, there are several states which are activated based on what’s going on in the game. For instance, if the player is running, then the “Jog” state gets activated. If the player decides to perform his “Sonic Punch” skill the “SuperPunchCharging” state gets activated (Super Punch is the internal name for Sonic Punch and is used throughout the code). Notice that you can enter a state from various points, for example both “Idle” and “Jog” states can transition to “SuperPunchCharging”. This is happening because the player can perform that skill while moving or by standing still.

If we take a look at the “Jog” state we’re going to see that in this state we actually have several different candidate animations to be played:

Click on image to enlarge on a new tab

Here’s what happens when the Equipped Weapon variable changes:

In the video above we’re seeing two different animations blending together in order to achieve the final result. For this state, I’m making sure that the animations blend together in a bone called “spine_03”. That means that the blendspace “BS_Run1D” transforms the bones below that particular bone, while the “Aim” animations transform the bones above that:

Click on image to enlarge on a new tab

According to the bone hierarchy above and the animation blending described in the Jog state, all the bones that have “spine_03” as their parent will follow the “aim” animations, while all the other bones will follow the “Bs_Run1D”.

The same story is more or less hapenning to each state. Each state is referencing the required variables in order to play the correct animation at any given moment. However, the whole state machine provides just the “base” animation for the character.

Anim Graph

In the animation graph I’m getting the result of the mentioned state machine and later I’m blending additional animations that only fire when an event is happening (for example if the player is hit or if the player decides to switch weapons). During these events the state machine doesn’t switch states however the output is completely different as we’re blending animations on top of the given state:

Click on image to enlarge on a new tab

Notice the “Slot X” nodes along the given graph. These nodes allow us to blend montages that belong to specific slots. For example, the UpperBody slot contains the animation that moves the hands of the character when the player is switching weapons.

The same logic with some minor modifications to accomodate specific scenarios is used to play animations for every character throughout the game.

I hope you found this dev log interesting! Would you like to read a dev log for a specific system of the game? If so, leave a comment down below!
Thanks for reading this and have a nice day! Until next time!

The post Mark One Dev Log #3 – Overview of the Animation System first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/mark-one-dev-log-3-overview-of-the-animation-system/feed/ 0 4058
Mark One Dev Log #2 – The Mind of Ranged Enemies https://www.orfeasel.com/mark-one-dev-log-2-the-mind-of-ranged-enemies/?utm_source=rss&utm_medium=rss&utm_campaign=mark-one-dev-log-2-the-mind-of-ranged-enemies https://www.orfeasel.com/mark-one-dev-log-2-the-mind-of-ranged-enemies/#comments_reply Mon, 19 Oct 2020 09:05:35 +0000 https://www.orfeasel.com/?p=4028 Welcome to the Mark One Dev Log #2! In this post I’m going to about the logic that drives ranged AI enemies in the game. Before I dive into more details about it in case you want to support my project you can wishlist on Steam using the following link: Rules of ranged enemies Each…

The post Mark One Dev Log #2 – The Mind of Ranged Enemies first appeared on Orfeas Eleftheriou.]]>
Welcome to the Mark One Dev Log #2! In this post I’m going to about the logic that drives ranged AI enemies in the game. Before I dive into more details about it in case you want to support my project you can wishlist on Steam using the following link:

Rules of ranged enemies

Each enemy wants to find a suitable location in the level from which he can shoot at the player. This location needs to meet the following criteria:

  • The distance from the player must be <= than the attack range of each enemy (range varies between different types of enemies)
  • The player must be visible from the location that the AI wants to go, otherwise enemies will be shooting at walls and other types of blocking geometries
  • The location shouldn’t block other range enemies from shooting at the player

Considering the above criteria it makes sense that some form of communication between AIs has to be made in order for them to spread around the player and take up different locations in order to attack him. Unreal Engine has a tool called EQS (Enviroment Query System) which I thought it would be ideal for my use case. Unfortunately, while this system is great in the UE4 version that I’m using it’s still experimental and results in a lot of crashes during runtime, which is a deal breaker for my case (by the time of this writing Chris Murphy mentioned on Twitter that EQS isn’t stated as experimental in 4.26). This meant that I had to develop my own system that would allow my enemies to communicate with each other and find the ideal spot.

Behavior of ranged enemies

Every single ranged enemy in the game has the same behavior and the differences come from various parameters such as Damage, Attack Range, Movement Speed etc.. Let’s take a look at the Behavior Tree for their logic

Behavior Tree of Ranged Enemies. Click on image to enlarge in a new tab.

The logic behind the AI is the following:

  • If the pawn is currently patrolling a path, move along the path and when it reaches a patrol node a task just assigns the next patrol node to keep the pawn moving
  • If the AI is alive and has detected the player (in the Behavior Tree context this is the FixatedTarget key) then the enemy is currently in combat.

Combat logic of ranged enemies

When the ranged enemies are in combat first they check if they’re out of their attack range. If that’s the case, then they spring their way through until they reach a certain amount of units far away from the player (this distance varies between enemies that have different attack range).

When enemies are in attack range, their first priority is to “Find a Shooting Spot”, meaning to find a location that meets the criteria I listed two in the Rules of Ranged Enemies section. The “Find Shooting Spot” behavior tree task is a C++ task that created in order to replace the EQS system.

Find Shooting Spot Behavior Tree Task

The logic behind this task is summed up with the following points:

  1. First we check if the current location of the enemy instance meets all the criteria. If that’s the case then we don’t try to find another location since we’re good to go.
  2. If the current location of the enemy isn’t valid for the reasons I listed above then a list of candidate locations is generated around the player. At this step, in each location there’s a visibility trace test going on to verify that the player can be seen from that location. If that test is valid we add the location of our list.
  3. Once the candidate location list is generated, we’re performing an ascending sort of the locations starting from the one which is closest to the AI that is trying to find a new shooting location.
  4. When the list is sorted the AI will select the closest point that doesn’t have a nearby ranged enemy and that is not blocking any other ranged enemy from shooting at the player.
  5. Once a selection has been made, we’re registering the location as “occupied” to a component that has a static list of locations with the assigned AIs. At this point it’s worth mentioning that each time an enemy moves from its location it unregisters that location from the aforementioned list

At this point, once the “Find Shooting Spot” behavior tree finds a valid location in the world, it updates the AI’s blackboard with it and from the logic contained in the Behavior Tree the enemy will start moving to the new location while shooting. If the AI shoots by mistake one of its friendlies then we discard the damage (no friendly fire allowed!).

To sum up the wall of text above, here’s a short video that visualizes the tested locations in a testing map:

Thank you for reading my new dev log! Until next time!

The post Mark One Dev Log #2 – The Mind of Ranged Enemies first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/mark-one-dev-log-2-the-mind-of-ranged-enemies/feed/ 0 4028
Mark One Dev Log #1 https://www.orfeasel.com/mark-one-dev-log-1/?utm_source=rss&utm_medium=rss&utm_campaign=mark-one-dev-log-1 https://www.orfeasel.com/mark-one-dev-log-1/#comments_reply Mon, 12 Oct 2020 09:21:33 +0000 https://www.orfeasel.com/?p=4004 Mark One is a game that I have been working on for the last few months. It’s a top down Sci-Fi shooter game, heavily influenced by Doom, with twin stick shooter elements. In this post I’m going to talk about the tutorial level of the game and how important it’s for the players. But before…

The post Mark One Dev Log #1 first appeared on Orfeas Eleftheriou.]]>
Mark One is a game that I have been working on for the last few months. It’s a top down Sci-Fi shooter game, heavily influenced by Doom, with twin stick shooter elements. In this post I’m going to talk about the tutorial level of the game and how important it’s for the players. But before we dive into that, in case you want to support my development you can help me by wishlisting the game on Steam in the following link

Identifying issues from the beta version

When I first side-loaded the game in Steam I included a tutorial level which would launch once the player would click the “New Game” option. Ideally a tutorial level has to teach every core mechanic and aspect of the game to new players. Players who didn’t understand or didn’t learn the core mechanics of the game usually get stuck along the way since they find it “too difficult” (based on the feedback that I have received from various beta testers).

After asking the testers why they found the game too difficult I identified the following issues:

  • Players didn’t understand they could generate ammo without relying to ammo drops
  • Players didn’t realise that during dash they were immune to any damage
  • Players didn’t understand they could get back up to full HP using their “Sonic Punch” skill.

The root of this problem was the poorly designed tutorial level. Even though decals were placed all around the level explaining the mechanics the player didn’t notice them or didn’t read them at all. This resulted in poor experience overall.

First implementation of the tutorial level

On the screenshot above, you can notice how decals are placed all over the place however they’re not going into depth on what’s going on. At this point, the only thing that the player has to do it to get back up to full HP in order for the door to open up. This doesn’t go into depth of the mechanics at all.

Resolving identified issues

In order to resolve the issues mentioned above I decided that a complete overhaul of the tutorial level was in order. To tackle all the issues above I redesigned the level and included several small rooms that each one of them having a different objective. Only after completing the objective the player is able to move to the next room.

New tutorial level. Red highlighted areas describe the objective of each room. Yellow highlighted area is a portal which is hidden in the game by default and gets activated once the player completes the level.

So after implementing the level above I felt like I needed to include an additional system to make sure that the players can understand the mechanics betters. So I wondered, what should I do in order to clarify things further? Apparently, Doom devs had already provided the answer to that question:

Doom Eternal Marauder Tutorial

Doom contains tutorial elements throughout their gameplay. For example, a window similar to the one above pops up each time the player has to know about new mechanics or about information the they need to know in order to proceed and surpass the problem they’re currently facing. The frame above the description contains a video that plays on a loop until the player dismisses the tutorial from the screen to get back to playing the game.

Since I find that this system works best for my case as well, I implemented it for Mark One as well. Here are some short videos from some tutorials that appear in the revamped tutorial level

Dash tutorial
Ammo tutorial
Sonic Punch tutorial

Thanks for reading this! What additional systems should I implement in order for the tutorials to be clearer and provide a better user experience overall? Let me know in the comments below. Until next time!

The post Mark One Dev Log #1 first appeared on Orfeas Eleftheriou.]]>
https://www.orfeasel.com/mark-one-dev-log-1/feed/ 0 4004