Build a Sci-Fi Arena in Unreal Engine 5
Build a C++ third-person combat arena with AI enemies in Unreal Engine 5.
Introduction
30 Second Summary
Every sci-fi shooter you've played has the same building blocks. A character that moves, a weapon that fires, and enemies that fight back.
In this project, you will build a playable sci-fi combat arena in Unreal Engine 5 using C++. You'll create a third-person character with sprint and dash, a laser weapon system, and AI enemies that patrol and chase you.
What You'll Build
A third-person sci-fi combat prototype featuring a custom C++ character, a line trace weapon, and AI enemies driven by Behavior Trees.
By the end of this project, you'll have:
- A custom C++ Character class with sprint, dash, and jump abilities using the Enhanced Input System.
- A working laser weapon system that uses line traces and Unreal's damage framework to destroy enemies.
- AI enemies that patrol the arena and chase you on sight using Behavior Trees.
- Secret Mission: add a physics-based plasma grenade projectile with area damage.
Are there any prerequisites?
You need basic C++ knowledge (variables, functions, classes). No prior Unreal Engine experience is required.
Install Unreal Engine and Set Up Your Compiler
Before you can build a sci-fi combat arena, you need the game engine itself and a C++ compiler. Unreal Engine is the industry-standard engine for AAA game development, and it compiles your C++ code into a playable game.
In this step, you will install Unreal Engine 5, set up the C++ compiler for your operating system, and add the VS Code extensions that give you code completion and IntelliSense for Unreal's massive codebase.
In this step, get ready to:
- Install Epic Games Launcher and Unreal Engine 5.
- Install the C++ compiler toolset for your operating system.
- Install VS Code extensions for C++ and Unreal Engine development.
Install Epic Games Launcher and Unreal Engine
Unreal Engine is free to download and use. A 5% royalty only applies after your product earns over $1 million USD in lifetime gross revenue.
Windows and Linux in this project
This project supports Windows and Linux. On Windows, you download Unreal Engine through the Epic Games Launcher. On Linux, the Epic Games Launcher is not available, so you build Unreal Engine from the official GitHub source.
The instructions below are split by platform. Follow the tab that matches your operating system.
Windows
- Open your browser and navigate to the Unreal Engine download page:
https://www.unrealengine.com/download
- Click Download to download the Epic Games Launcher installer.
- Run the installer and follow the on-screen prompts to install the Epic Games Launcher.
- When the launcher opens, create a new Epic Games account or sign in with an existing one.
- In the Epic Games Launcher, click the Unreal Engine tab on the left sidebar.
- Click Install Engine to begin downloading Unreal Engine 5.
- Accept the default installation location or click Browse to choose a different folder.
- Click Install to start the download.
How long does this take?
The Unreal Engine download is 40-60 GB depending on which components you select. On a typical home internet connection, expect this to take 30 minutes to a few hours.
You can continue with the compiler and extension setup below while it downloads.
- Once the download completes, you should see a Launch button next to the installed engine version in the launcher.
Linux
On Linux, you need to build Unreal Engine from the official source on GitHub. This requires linking your GitHub account to your Epic Games account for repository access.
- Create an Epic Games account at the Unreal Engine website if you do not have one:
https://www.unrealengine.com
- Follow Epic's official guide to link your GitHub account and clone the Unreal Engine source repository:
https://dev.epicgames.com/documentation/unreal-engine/downloading-source-code-in-unreal-engine
- Once you have the source, navigate to the root directory and run the setup script:
cd ~/UnrealEngine
./Setup.sh
- Generate the project files by running:
./GenerateProjectFiles.sh
- Build the engine by running:
make
How long does this take?
Building from source requires at least 100 GB of disk space. Depending on your system, the build can take anywhere from 10 minutes to over an hour.
You need at least 32 GB RAM, a quad-core 2.5 GHz+ CPU, and a GPU with 8 GB VRAM (GeForce RTX 2080 or equivalent). The Vulkan RHI is required on Linux.
- Verify the build succeeded by launching the editor:
cd Engine/Binaries/Linux
./UnrealEditor
If the Unreal Editor window opens, your installation is working. Close it for now.
Having trouble with the installation?
Make sure you have enough free disk space (at least 60 GB on Windows, 100 GB on Linux). On Windows, try running the installer as Administrator if it fails.
On Linux, make sure your GitHub account is linked to your Epic Games account before trying to clone the repository.
help me troubleshoot my Unreal Engine 5 installation
Install your compiler toolset
Unreal Engine needs a C++ compiler to turn your code into a running game. The compiler you need depends on your operating system.
Windows
On Windows, Unreal Engine uses MSVC (Microsoft Visual C++) from the Build Tools for Visual Studio 2022. You do not need a full Visual Studio installation to use VS Code.
- Open your browser and navigate to the Visual Studio Downloads page:
https://visualstudio.microsoft.com/downloads
- Scroll down to the All Downloads section.
- Expand Tools for Visual Studio.
- Click Download next to Build Tools for Visual Studio 2022.
- Run the downloaded installer.
- In the installer window, check the box for Desktop development with C++ workload.
- Click Install and wait for the installation to complete.
What does this install?
The "Desktop development with C++" workload installs the MSVC compiler, linker, Windows SDK, and standard libraries. These are what Unreal Engine calls when it compiles your C++ game code.
After installation completes, you can close the Visual Studio Installer window.
Linux
On Linux, Unreal Engine uses Clang as its compiler. You also need to run a toolchain setup script from the engine directory.
- Install Clang by running this command:
sudo apt install clang
- Navigate to your Unreal Engine install directory and run the toolchain setup script:
cd ~/UnrealEngine
./Build/BatchFiles/Linux/SetupToolchain.sh
What does SetupToolchain.sh do?
This script configures Unreal Engine to use the Clang compiler you just installed. It sets up paths and build configuration so the engine knows where to find your compiler when building C++ projects.
- Verify Clang installed correctly by running:
clang --version
You should see a version number printed in the terminal output.
Compiler installation not working?
On Windows, make sure you selected the "Desktop development with C++" workload specifically. Without it, the MSVC compiler will not be installed.
On Linux, if apt cannot find the clang package, try running sudo apt update first.
help me fix my C++ compiler setup for Unreal Engine
Install VS Code extensions for C++ and Unreal Engine
VS Code needs two extensions to understand Unreal Engine's C++ code and build system. These give you IntelliSense (code completion), error highlighting, and the ability to navigate Unreal's source.
- Press the Windows key (Windows) or Super key (Linux) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the Extensions panel by pressing Ctrl+Shift+X.
- Search for C/C++ Extension Pack and click Install on the one published by Microsoft.
- Search for C# and click Install on the one published by Microsoft.
Why do I need the C# extension for a C++ project?
Unreal Engine's build system (UnrealBuildTool) is written in C#. The C# extension allows VS Code to read and understand the build system source, which helps with project generation and build configurations.
- Verify both extensions appear in your installed extensions list by checking the Extensions panel sidebar.
You should see both C/C++ Extension Pack and C# listed with a gear icon indicating they are installed and active.
Extensions not appearing?
Make sure you are searching the marketplace (not your installed list). If the install button is greyed out, try restarting VS Code and checking your internet connection.
help me install VS Code extensions for Unreal Engine C++ development
Your development environment is ready. You have Unreal Engine 5 installed, a working C++ compiler, and VS Code configured for Unreal development. Next up, you will create your C++ project and connect it to VS Code.
Create the C++ Project and Configure VS Code
Your development environment is ready to go. You have Unreal Engine installed, a compiler toolset configured, and VS Code loaded with the right extensions.
Now it is time to create your actual game project. You will set up a C++ project from a template, connect it to VS Code, and verify that code completion works before writing a single line of gameplay code.
In this step, get ready to:
- Create a new C++ project in Unreal Engine from the Third Person template.
- Set VS Code as your default source code editor.
- Generate a VS Code workspace and verify IntelliSense is working.
Create a new C++ project
Unreal Engine's Project Browser lets you pick a starting template so you are not building from a blank screen. The Third Person template gives you a working camera, character mesh, and test level out of the box.
- Press the Windows key (Windows) or the Super key (Linux) to open your search bar.
- Type Epic Games Launcher and press Enter to open it.
- In the Epic Games Launcher, click the Unreal Engine tab on the left sidebar.
- Click Launch next to your installed engine version to open Unreal Engine.
The Unreal Project Browser opens automatically. This is where you configure your new project.
- Select Games as the development category.
- Select the Third Person template.
- Under Project Defaults, set the implementation to C++.
- Set the target platform to Desktop.
- Set the quality preset to Maximum.
- Enable Starter Content.
- In the project name field, type NovaStar.
- Click Create.
Unreal Engine generates your project files and opens the editor. You should see a 3D viewport with the default Third Person level loaded, including a floor, some walls, and a mannequin character.
Why the Third Person template?
Starting from this template gives you a working camera and character mesh as a reference. You will replace the default Blueprint character with your own C++ class in the next step, learning by reconstructing what the template provides.
This gets you to playable gameplay faster than starting from a blank world where you would need to set up a camera, input, and character from scratch.
Project creation taking a long time?
The first time you create a C++ project, Unreal Engine compiles the engine's source headers for your project. This can take several minutes depending on your machine. Do not close the editor while it is loading.
If the editor closes or you see a compilation error, make sure your compiler toolset from Step 1 is fully installed. On Windows, confirm the Desktop development with C++ workload is present in the Visual Studio Installer.
help me troubleshoot my Unreal Engine C++ project creation error
Set VS Code as your default editor
By default, Unreal Engine tries to open code files in Visual Studio (the full IDE). Since you are using VS Code, you need to tell the editor to use it instead.
- In the Unreal Editor menu bar, click Edit.
- Click Editor Preferences.
- In the left panel, expand General and click Source Code.
- Set the Source Code Editor dropdown to Visual Studio Code.
- Close and reopen the Unreal Editor for the change to take effect.
After restarting, double-clicking any C++ file in the editor will open it in VS Code instead of Visual Studio.
Why restart the editor?
Unreal Engine caches the editor preference for which IDE to launch. The restart ensures all internal paths update to point at VS Code. Without it, some actions (like opening header files from error logs) may still try to use the old editor.
Generate the VS Code workspace
Unreal Engine can generate a workspace file that configures VS Code with the correct include paths, build tasks, and IntelliSense settings for your project.
- In the Unreal Editor menu bar, click Tools.
- Click Refresh Visual Studio Code Project.
This generates a .code-workspace file in your project directory.
- Open the generated .code-workspace file in VS Code. You can find it in your NovaStar project folder.
- In VS Code, expand the Source folder in the Explorer sidebar and open any .cpp file.
- Type a few characters inside a function body and confirm that code completion suggestions appear.
If you see autocomplete suggestions showing Unreal Engine types and functions, IntelliSense is working correctly.
Linux alternative for workspace generation
On Linux, you can also generate the workspace from the terminal. Navigate to your engine install directory and run:
./GenerateProjectFiles.sh -vscode
This produces the same .code-workspace file as the menu option in the editor.
No code completion showing up?
Make sure you opened the .code-workspace file (not just the folder). The workspace file contains the include paths that IntelliSense needs to find Unreal Engine headers.
If suggestions still do not appear after a minute, check that the C/C++ Extension Pack is installed and enabled in VS Code. IntelliSense may take some time to index a large project on first open.
help me fix IntelliSense not working in VS Code for my Unreal Engine project
Your project is created and your code editor is ready. Next up, you will build a custom character class with movement controls powered by the Enhanced Input system.
Create Your Sci-Fi Character C++ Class with Enhanced Input
Every game needs a player character, and in Unreal Engine 5, building one from C++ gives you full control over movement, camera behavior, and input handling.
In this step, you will create a custom NovaCharacter class with a third-person camera system and wire up the Enhanced Input system for movement, sprinting, and dashing.
In this step, get ready to:
- Create a C++ Character class with a spring arm camera system.
- Bind Enhanced Input actions for movement, sprinting, and dashing.
- Implement movement callbacks that respond to your keyboard and mouse.
Create the character class and set up the camera
Unreal Engine's ACharacter class provides built-in movement, collision, and animation support. You will derive your own class from it to customize everything.
- In Unreal Editor, click Tools in the top menu bar.
- Click New C++ Class.
- In the class wizard, select Character as the parent class and click Next.
- Name the class NovaCharacter and click Create Class.
What just happened?
The C++ Class Wizard created two files: NovaCharacter.h (the header) and NovaCharacter.cpp (the source). Live Coding automatically compiled the empty class in the background.
The files open in VS Code. You will replace their contents with your custom character code.
- In VS Code, open NovaCharacter.h and replace its entire contents with the following code:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "NovaCharacter.generated.h"
// Forward declarations for component types
class USpringArmComponent;
class UCameraComponent;
UCLASS()
class NOVASTAR_API ANovaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ANovaCharacter();
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// Camera components
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
UCameraComponent* FollowCamera;
};
What does this code do?
This header declares your character class with two camera components. USpringArmComponent acts like a selfie stick that keeps the camera at a fixed distance behind the player. UCameraComponent is the actual camera that renders what the player sees.
The UPROPERTY macro exposes these to Unreal's reflection system so the engine can manage their lifecycle and display them in the editor.
- Now open NovaCharacter.cpp and replace its contents with the constructor code:
#include "NovaCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
ANovaCharacter::ANovaCharacter()
{
// Create the spring arm and attach to root
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->bUsePawnControlRotation = true;
// Create the camera and attach to the spring arm
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom);
// Let the character rotate toward movement direction instead of camera
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
// Movement tuning
GetCharacterMovement()->MaxWalkSpeed = 600.f;
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
}
What does the constructor do?
The constructor runs once when the character is created. It builds the spring arm and camera, then configures movement.
- bUsePawnControlRotation = true makes the camera follow your mouse rotation.
- bOrientRotationToMovement = true rotates the character mesh to face the direction of movement.
- MaxWalkSpeed, JumpZVelocity, and AirControl set the baseline feel for a responsive sci-fi soldier.
- Save both files in VS Code.
Seeing red squiggles in VS Code?
IntelliSense may take a moment to index Unreal Engine headers. If errors persist after 10-15 seconds, try running Tools > Refresh Visual Studio Code Project in Unreal Editor to regenerate the workspace.
Why aren't my IntelliSense suggestions working?
Set up Enhanced Input bindings in C++
The Enhanced Input system uses data assets called Input Actions and Mapping Contexts instead of hard-coded key bindings. You will declare pointers to these assets in the header, then bind them to callback functions in the source file.
- In NovaCharacter.h, replace the entire file contents with this updated version that adds Input Action declarations:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "NovaCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
UCLASS()
class NOVASTAR_API ANovaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ANovaCharacter();
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// Camera components
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
UCameraComponent* FollowCamera;
// Input Mapping Context
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputMappingContext* IMC_Nova;
// Input Actions
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Move;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Look;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Jump;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Sprint;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Dash;
// Callback functions
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartSprint();
void StopSprint();
void Dash();
};
What changed in the header?
- Added #include "InputActionValue.h" and forward declarations for UInputMappingContext and UInputAction.
- Declared one Mapping Context pointer (IMC_Nova) that maps physical keys to logical actions.
- Declared five Input Action pointers for Move, Look, Jump, Sprint, and Dash.
- Declared five callback functions that Unreal calls when those actions fire.
- Now add the SetupPlayerInputComponent function to NovaCharacter.cpp. Place this below the constructor:
void ANovaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Cast to Enhanced Input and bind each action
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EIC->BindAction(IA_Move, ETriggerEvent::Triggered, this, &ANovaCharacter::Move);
EIC->BindAction(IA_Look, ETriggerEvent::Triggered, this, &ANovaCharacter::Look);
EIC->BindAction(IA_Jump, ETriggerEvent::Started, this, &ACharacter::Jump);
EIC->BindAction(IA_Jump, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EIC->BindAction(IA_Sprint, ETriggerEvent::Started, this, &ANovaCharacter::StartSprint);
EIC->BindAction(IA_Sprint, ETriggerEvent::Completed, this, &ANovaCharacter::StopSprint);
EIC->BindAction(IA_Dash, ETriggerEvent::Started, this, &ANovaCharacter::Dash);
}
}
What does BindAction do?
Each BindAction call connects an Input Action asset to a C++ function. The ETriggerEvent controls when the callback fires.
- Triggered fires every frame the key is held (good for continuous movement).
- Started fires once on key press (good for one-shot actions like jump or dash).
- Completed fires once on key release (good for stopping sprint).
- Add the BeginPlay function below SetupPlayerInputComponent in NovaCharacter.cpp:
void ANovaCharacter::BeginPlay()
{
Super::BeginPlay();
// Register the input mapping context so the engine knows our key bindings
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(IMC_Nova, 0);
}
}
}
Why add the mapping context in BeginPlay?
The Enhanced Input system needs to know which key mappings to use at runtime. AddMappingContext registers your mapping context with the local player's input subsystem. The second parameter (0) is the priority. Lower numbers mean higher priority.
- At the top of NovaCharacter.cpp, make sure you have all the required includes. The full include block should look like this:
#include "NovaCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
- Save NovaCharacter.h and NovaCharacter.cpp.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full header
Your NovaCharacter.h should look exactly like this:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "NovaCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
UCLASS()
class NOVASTAR_API ANovaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ANovaCharacter();
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
UCameraComponent* FollowCamera;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputMappingContext* IMC_Nova;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Move;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Look;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Jump;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Sprint;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Dash;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartSprint();
void StopSprint();
void Dash();
};
Create Input assets and implement movement
Your C++ code references Input Action assets that do not exist yet. You need to create them in the Content Browser so Unreal can connect your key presses to the callback functions.
- In Unreal Editor, open the Content Browser (click the Content Drawer button at the bottom of the editor).
- Right-click in an empty area of the Content Browser.
- Select Input, then Input Action.
- Name it IA_Move. Double-click to open it and set its Value Type to Axis2D (Vector2D). Save and close.
- Repeat to create IA_Look with Value Type Axis2D (Vector2D).
- Create IA_Jump with Value Type Bool.
- Create IA_Sprint with Value Type Bool.
- Create IA_Dash with Value Type Bool.
- Now create the Mapping Context. Right-click in the Content Browser.
- Select Input, then Input Mapping Context.
- Name it IMC_Nova.
- Double-click IMC_Nova to open it.
- Click the + button next to Mappings to add a mapping. Select IA_Move as the action. Add keys W, A, S, D with appropriate Modifiers (Negate for S, Swizzle for A/D).
- Add another mapping for IA_Look and assign Mouse XY 2D-Axis.
- Add IA_Jump mapped to Space Bar.
- Add IA_Sprint mapped to Left Shift.
- Add IA_Dash mapped to E.
- Save IMC_Nova.
What is an Input Mapping Context?
A Mapping Context is a data asset that maps physical keys to logical Input Actions. It separates your game logic ("the player wants to move") from the hardware ("W was pressed").
You can swap Mapping Contexts at runtime to support different control schemes without changing any C++ code.
- Back in VS Code, add the movement callback functions to the bottom of NovaCharacter.cpp, after the BeginPlay function:
void ANovaCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller)
{
// Get the direction the camera is facing (yaw only)
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
// Calculate forward and right directions relative to camera
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// Apply movement input
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
Why use the controller's yaw rotation?
When you press W, you want to move in the direction the camera is facing, not the direction the character mesh is facing. By extracting only the Yaw from the controller rotation, you get a flat forward vector that ignores camera pitch. This makes movement feel natural in a third-person game.
- Add the Look callback below the Move function:
void ANovaCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
What does Look do?
This rotates the camera (via the controller) based on mouse movement. AddControllerYawInput turns left/right. AddControllerPitchInput tilts up/down. Because the spring arm has bUsePawnControlRotation = true, the camera follows this rotation.
- Finally, add the Sprint and Dash functions:
void ANovaCharacter::StartSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 1200.f;
}
void ANovaCharacter::StopSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 600.f;
}
void ANovaCharacter::Dash()
{
// Launch the character forward at high speed
FVector DashDirection = GetActorForwardVector();
LaunchCharacter(DashDirection * 1500.f, true, false);
}
How do Sprint and Dash work?
- Sprint toggles MaxWalkSpeed between 600 (normal) and 1200 (sprinting). Releasing Shift resets it.
- Dash uses LaunchCharacter to apply a one-time impulse of 1500 units in the character's forward direction. The true parameter overrides the current horizontal velocity for a snappy burst.
- Save NovaCharacter.cpp.
- Compile the project. In Unreal Editor, press Ctrl+Alt+F11 to trigger Live Coding. You should see a notification in the bottom-right corner confirming the build succeeded.
Build failed?
Check the Live Coding console output for the specific error. Common issues include missing semicolons, mismatched angle brackets in template parameters, or forgetting an include.
If Live Coding reports header-related errors, close Unreal Editor, rebuild from VS Code using Ctrl+Shift+B, then reopen the editor. Header changes sometimes require a full rebuild rather than Live Coding.
Help me fix my compilation error.
✔️ Awesome, I've got everything!
Great. Make sure your project compiled without errors before continuing.
ⓧ I'd like to double check the full code
Your complete NovaCharacter.cpp should look like this:
#include "NovaCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
ANovaCharacter::ANovaCharacter()
{
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom);
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->MaxWalkSpeed = 600.f;
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
}
void ANovaCharacter::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(IMC_Nova, 0);
}
}
}
void ANovaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EIC->BindAction(IA_Move, ETriggerEvent::Triggered, this, &ANovaCharacter::Move);
EIC->BindAction(IA_Look, ETriggerEvent::Triggered, this, &ANovaCharacter::Look);
EIC->BindAction(IA_Jump, ETriggerEvent::Started, this, &ACharacter::Jump);
EIC->BindAction(IA_Jump, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EIC->BindAction(IA_Sprint, ETriggerEvent::Started, this, &ANovaCharacter::StartSprint);
EIC->BindAction(IA_Sprint, ETriggerEvent::Completed, this, &ANovaCharacter::StopSprint);
EIC->BindAction(IA_Dash, ETriggerEvent::Started, this, &ANovaCharacter::Dash);
}
}
void ANovaCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void ANovaCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
void ANovaCharacter::StartSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 1200.f;
}
void ANovaCharacter::StopSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 600.f;
}
void ANovaCharacter::Dash()
{
FVector DashDirection = GetActorForwardVector();
LaunchCharacter(DashDirection * 1500.f, true, false);
}
Your character class is ready with a full camera system, movement, sprint, and dash. Next up, you will build a laser weapon system using line traces to shoot at enemies.
Build the Laser Weapon System with Line Traces
Your character can move, sprint, dash, and jump. Now it needs a way to fight back.
In this step, you will build a laser weapon that fires an invisible ray from your camera into the world. When that ray hits something, it deals damage. This technique is called a line trace (also known as raycasting). It is the foundation of most hit-scan weapons in professional shooters.
In this step, get ready to:
- Create a C++ LaserWeapon class with damage, fire rate, and range properties.
- Implement line trace firing with debug visualization and damage application.
- Wire up an Input Action so clicking the mouse fires the weapon.
Create the weapon class
The weapon lives as its own Actor class. This keeps weapon logic separate from character logic, making it easy to swap weapons or add new ones later.
- In Unreal Editor, click Tools in the top menu bar.
- Click New C++ Class.
- In the Choose Parent Class menu, select Actor, then click Next.
- Name the class LaserWeapon and click Create Class.
What happens when you create a class?
The C++ Class Wizard generates two files: LaserWeapon.h (the header with declarations) and LaserWeapon.cpp (the source with implementations). Live Coding compiles them automatically.
- Once the files open in VS Code, replace the entire contents of LaserWeapon.h with the following code:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "LaserWeapon.generated.h"
UCLASS()
class NOVASTAR_API ALaserWeapon : public AActor
{
GENERATED_BODY()
public:
ALaserWeapon();
// Damage dealt per shot
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Weapon)
float Damage = 25.0f;
// Minimum time between shots in seconds
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Weapon)
float FireRate = 0.2f;
// How far the laser can reach in units
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Weapon)
float TraceRange = 5000.f;
// Called by the owning character to fire
void Fire();
private:
FTimerHandle FireTimerHandle;
bool bCanFire = true;
void ResetFire();
};
What does this header declare?
- Damage, FireRate, and TraceRange are editable weapon stats exposed to the Unreal Editor via UPROPERTY.
- Fire() is the public function the character calls when the player clicks.
- FireTimerHandle and bCanFire enforce the cooldown between shots so the weapon cannot fire every frame.
- ResetFire() is a private callback that re-enables firing after the cooldown expires.
- Save LaserWeapon.h.
Now you need to update NovaCharacter.h so your character knows about the weapon class, can spawn it, and can call its Fire() function.
- Open NovaCharacter.h in VS Code and replace its entire contents with the following:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "NovaCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
class ALaserWeapon;
UCLASS()
class NOVASTAR_API ANovaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ANovaCharacter();
// Public so LaserWeapon can access camera for line trace origin
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
UCameraComponent* FollowCamera;
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
USpringArmComponent* CameraBoom;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputMappingContext* IMC_Nova;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Move;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Look;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Jump;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Sprint;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Dash;
// Fire input action bound to left mouse button
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Fire;
// Class to spawn as the weapon
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Weapon)
TSubclassOf<ALaserWeapon> LaserWeaponClass;
// Reference to the spawned weapon instance
UPROPERTY()
ALaserWeapon* LaserWeapon;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartSprint();
void StopSprint();
void Dash();
void FireWeapon();
};
What changed in the header?
- A forward declaration class ALaserWeapon; tells the compiler this class exists without needing the full header yet.
- FollowCamera moved to the public section so the weapon can access it for its line trace origin.
- LaserWeaponClass lets you pick which weapon to spawn from the editor. LaserWeapon stores the spawned instance.
- IA_Fire and FireWeapon() connect the left mouse button to the weapon's Fire() function.
- Save NovaCharacter.h.
Next, update the character's source file to spawn the weapon on game start and connect firing to the input system.
- Open NovaCharacter.cpp in VS Code and replace its entire contents with the following:
#include "NovaCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "LaserWeapon.h"
ANovaCharacter::ANovaCharacter()
{
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom);
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->MaxWalkSpeed = 600.f;
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
}
void ANovaCharacter::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(IMC_Nova, 0);
}
}
if (LaserWeaponClass)
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
LaserWeapon = GetWorld()->SpawnActor<ALaserWeapon>(LaserWeaponClass, SpawnParams);
if (LaserWeapon)
{
LaserWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, TEXT("hand_rSocket"));
}
}
}
void ANovaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EIC->BindAction(IA_Move, ETriggerEvent::Triggered, this, &ANovaCharacter::Move);
EIC->BindAction(IA_Look, ETriggerEvent::Triggered, this, &ANovaCharacter::Look);
EIC->BindAction(IA_Jump, ETriggerEvent::Started, this, &ACharacter::Jump);
EIC->BindAction(IA_Jump, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EIC->BindAction(IA_Sprint, ETriggerEvent::Started, this, &ANovaCharacter::StartSprint);
EIC->BindAction(IA_Sprint, ETriggerEvent::Completed, this, &ANovaCharacter::StopSprint);
EIC->BindAction(IA_Dash, ETriggerEvent::Started, this, &ANovaCharacter::Dash);
EIC->BindAction(IA_Fire, ETriggerEvent::Triggered, this, &ANovaCharacter::FireWeapon);
}
}
void ANovaCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void ANovaCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
void ANovaCharacter::StartSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 1200.f;
}
void ANovaCharacter::StopSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 600.f;
}
void ANovaCharacter::Dash()
{
FVector DashDirection = GetActorForwardVector();
LaunchCharacter(DashDirection * 1500.f, true, false);
}
void ANovaCharacter::FireWeapon()
{
if (LaserWeapon)
{
LaserWeapon->Fire();
}
}
What is new in the source file?
- #include "LaserWeapon.h" gives access to the weapon class definition.
- The new block inside BeginPlay spawns the weapon actor and attaches it to the character's right hand socket.
- FireWeapon() is the callback that runs when the player clicks. It delegates to the weapon's Fire() function.
- The BindAction line for IA_Fire uses ETriggerEvent::Triggered so holding the mouse button fires continuously.
- Save NovaCharacter.cpp.
Seeing red squiggles in VS Code?
IntelliSense may show errors because LaserWeapon.cpp does not have its implementation yet. This is expected. You will write the implementation in the next substep.
help me fix IntelliSense errors in my Unreal Engine VS Code project
Implement line trace firing
The weapon's Fire() function shoots an invisible ray from the camera into the scene. If the ray hits an actor, damage is applied. A debug line renders in the viewport so you can see where your shots land.
- Open LaserWeapon.cpp in VS Code and replace its entire contents with the following:
#include "LaserWeapon.h"
#include "NovaCharacter.h"
#include "Camera/CameraComponent.h"
#include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h"
ALaserWeapon::ALaserWeapon()
{
PrimaryActorTick.bCanEverTick = false;
}
void ALaserWeapon::Fire()
{
if (!bCanFire) return;
// Get the owning character to access the camera
ANovaCharacter* Owner = Cast<ANovaCharacter>(GetOwner());
if (!Owner) return;
// Line trace starts at camera and extends forward by TraceRange
FVector Start = Owner->FollowCamera->GetComponentLocation();
FVector End = Start + Owner->FollowCamera->GetForwardVector() * TraceRange;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
Params.AddIgnoredActor(Owner);
bool bHit = GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, ECC_Visibility, Params);
if (bHit)
{
// Red line on hit, apply damage to whatever was struck
DrawDebugLine(GetWorld(), Start, HitResult.ImpactPoint, FColor::Red, false, 0.1f);
UGameplayStatics::ApplyDamage(HitResult.GetActor(), Damage, Owner->GetController(), this, UDamageType::StaticClass());
UE_LOG(LogTemp, Log, TEXT("Hit: %s"), *HitResult.GetActor()->GetName());
}
else
{
// Green line on miss
DrawDebugLine(GetWorld(), Start, End, FColor::Green, false, 0.1f);
UE_LOG(LogTemp, Log, TEXT("No hit"));
}
// Enforce fire rate cooldown
bCanFire = false;
GetWorldTimerManager().SetTimer(FireTimerHandle, this, &ALaserWeapon::ResetFire, FireRate, false);
}
void ALaserWeapon::ResetFire()
{
bCanFire = true;
}
How does the line trace work?
The trace starts at the camera's world position and extends forward by TraceRange units (5000 by default, about 50 meters). LineTraceSingleByChannel checks for the first object in that path.
On hit, ApplyDamage uses Unreal's built-in damage pipeline so any actor with a TakeDamage override receives the hit. The debug lines (red for hits, green for misses) last 0.1 seconds each.
The cooldown uses a timer handle. After each shot, bCanFire is set to false and a timer calls ResetFire() after 0.2 seconds to re-enable firing.
UE_LOG prints hit results to the Output Log so you can confirm your weapon is working.
- Save LaserWeapon.cpp.
Why line traces instead of projectile actors?
Line traces are simpler to implement and debug than spawning physics-based projectile actors. They teach the same damage pipeline concepts without the added complexity of projectile movement, collision, and lifecycle management.
Professional shooters use line traces for hit-scan weapons (pistols, rifles) and save projectile actors for slower-moving attacks like grenades or rockets.
Add fire rate cooldown
The firing logic is complete in your C++ code. Now you need to create the Input Action asset that triggers it, map it to the left mouse button, and compile everything.
- In the Content Browser, right-click and select Input then Input Action.
- Name the new asset IA_Fire.
- Double-click IA_Fire to open it. Set Value Type to Bool. Save and close.
What is an Input Action?
An Input Action is a data asset that represents a single player intention (fire, jump, move). It decouples the physical button from the code, so you can remap controls without changing C++.
- Open IMC_Nova in the Content Browser (double-click it).
- Click the + button to add a new mapping.
- Set the action to IA_Fire.
- Under the key binding, select Left Mouse Button.
- Save IMC_Nova.
- Compile the project by pressing Ctrl+Shift+B in VS Code, or use Live Coding in the Unreal Editor (Ctrl+Alt+F11).
Build failed with linker errors?
Make sure both LaserWeapon.h and LaserWeapon.cpp are saved. A missing .cpp file causes "unresolved external symbol" errors because the header declares functions the linker cannot find.
Also verify that #include "LaserWeapon.h" appears at the top of NovaCharacter.cpp. Without it, the compiler does not know what ALaserWeapon is.
help me fix unresolved external symbol linker errors in my Unreal Engine C++ project
Now test the weapon. You need some targets to shoot.
- In the Unreal Editor viewport, click the Place Actors panel (or press Shift+1).
- Drag 2-3 Cube static meshes from Starter Content into the level near the Player Start.
- Press Alt+P to play in the editor.
- Click the left mouse button to fire. You should see green debug lines when missing and red lines when hitting a cube.
- Open the Output Log (Window then Output Log) to confirm "Hit: Cube" messages appear.
Not seeing debug lines when firing?
Make sure IA_Fire is assigned in the character's Details panel. Select your character Blueprint (or the Default Pawn in the World Outliner), expand the Input category, and assign IA_Fire to the corresponding slot.
Also check that LaserWeaponClass is set in the Weapon category of your character. Without it, the weapon never spawns.
help me debug why my weapon is not firing in Unreal Engine
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files: LaserWeapon.h, LaserWeapon.cpp, and NovaCharacter.cpp.
ⓧ I'd like to double check the full code
Here is the full code for each file at this point in the project.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "LaserWeapon.generated.h"
UCLASS()
class NOVASTAR_API ALaserWeapon : public AActor
{
GENERATED_BODY()
public:
ALaserWeapon();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Weapon)
float Damage = 25.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Weapon)
float FireRate = 0.2f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Weapon)
float TraceRange = 5000.f;
void Fire();
private:
FTimerHandle FireTimerHandle;
bool bCanFire = true;
void ResetFire();
};
#include "LaserWeapon.h"
#include "NovaCharacter.h"
#include "Camera/CameraComponent.h"
#include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h"
ALaserWeapon::ALaserWeapon()
{
PrimaryActorTick.bCanEverTick = false;
}
void ALaserWeapon::Fire()
{
if (!bCanFire) return;
ANovaCharacter* Owner = Cast<ANovaCharacter>(GetOwner());
if (!Owner) return;
FVector Start = Owner->FollowCamera->GetComponentLocation();
FVector End = Start + Owner->FollowCamera->GetForwardVector() * TraceRange;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
Params.AddIgnoredActor(Owner);
bool bHit = GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, ECC_Visibility, Params);
if (bHit)
{
DrawDebugLine(GetWorld(), Start, HitResult.ImpactPoint, FColor::Red, false, 0.1f);
UGameplayStatics::ApplyDamage(HitResult.GetActor(), Damage, Owner->GetController(), this, UDamageType::StaticClass());
UE_LOG(LogTemp, Log, TEXT("Hit: %s"), *HitResult.GetActor()->GetName());
}
else
{
DrawDebugLine(GetWorld(), Start, End, FColor::Green, false, 0.1f);
UE_LOG(LogTemp, Log, TEXT("No hit"));
}
bCanFire = false;
GetWorldTimerManager().SetTimer(FireTimerHandle, this, &ALaserWeapon::ResetFire, FireRate, false);
}
void ALaserWeapon::ResetFire()
{
bCanFire = true;
}
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "NovaCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
class ALaserWeapon;
UCLASS()
class NOVASTAR_API ANovaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ANovaCharacter();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
UCameraComponent* FollowCamera;
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
USpringArmComponent* CameraBoom;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputMappingContext* IMC_Nova;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Move;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Look;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Jump;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Sprint;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Dash;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Fire;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Weapon)
TSubclassOf<ALaserWeapon> LaserWeaponClass;
UPROPERTY()
ALaserWeapon* LaserWeapon;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartSprint();
void StopSprint();
void Dash();
void FireWeapon();
};
#include "NovaCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "LaserWeapon.h"
ANovaCharacter::ANovaCharacter()
{
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom);
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->MaxWalkSpeed = 600.f;
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
}
void ANovaCharacter::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(IMC_Nova, 0);
}
}
if (LaserWeaponClass)
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
LaserWeapon = GetWorld()->SpawnActor<ALaserWeapon>(LaserWeaponClass, SpawnParams);
if (LaserWeapon)
{
LaserWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, TEXT("hand_rSocket"));
}
}
}
void ANovaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EIC->BindAction(IA_Move, ETriggerEvent::Triggered, this, &ANovaCharacter::Move);
EIC->BindAction(IA_Look, ETriggerEvent::Triggered, this, &ANovaCharacter::Look);
EIC->BindAction(IA_Jump, ETriggerEvent::Started, this, &ACharacter::Jump);
EIC->BindAction(IA_Jump, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EIC->BindAction(IA_Sprint, ETriggerEvent::Started, this, &ANovaCharacter::StartSprint);
EIC->BindAction(IA_Sprint, ETriggerEvent::Completed, this, &ANovaCharacter::StopSprint);
EIC->BindAction(IA_Dash, ETriggerEvent::Started, this, &ANovaCharacter::Dash);
EIC->BindAction(IA_Fire, ETriggerEvent::Triggered, this, &ANovaCharacter::FireWeapon);
}
}
void ANovaCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void ANovaCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
void ANovaCharacter::StartSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 1200.f;
}
void ANovaCharacter::StopSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 600.f;
}
void ANovaCharacter::Dash()
{
FVector DashDirection = GetActorForwardVector();
LaunchCharacter(DashDirection * 1500.f, true, false);
}
void ANovaCharacter::FireWeapon()
{
if (LaserWeapon)
{
LaserWeapon->Fire();
}
}
Your character now has a working laser weapon. Next up, you will add health, a damage system, and a HUD so you can see the impact of incoming fire.
Implement Health, Damage, and a HUD
Your character can move, sprint, dash, and fire a laser. But right now, nothing can hurt them back. In any combat game, the player needs a health pool that depletes when they take damage.
In this step, you will add a health system to your character, override Unreal's built-in TakeDamage function, and create a heads-up display (HUD) so the player can see their health in real time.
In this step, get ready to:
- Add health properties and a damage handler to your character.
- Create a HUD widget that displays health and ammo.
- Wire up a custom GameMode to display the HUD and verify damage works.
Add health and damage to the character
Unreal Engine has a built-in damage pipeline. Any actor can receive damage through the TakeDamage virtual function. By overriding it in your character, you control exactly what happens when damage arrives.
- In VS Code, open NovaCharacter.h.
- Find the public section where FollowCamera is declared. Add the following health properties and the TakeDamage override below it:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Health)
float CurrentHealth = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Health)
float MaxHealth = 100.f;
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
What does this code do?
CurrentHealth tracks the player's current hit points. MaxHealth stores the ceiling so you can calculate a percentage for the HUD progress bar.
The TakeDamage override tells Unreal: when this character receives damage from ApplyDamage, run our custom logic instead of the default empty handler.
- Save NovaCharacter.h.
Now you need to implement the logic that actually subtracts health and ends the game at zero.
- Open NovaCharacter.cpp.
- Scroll to the bottom of the file, after the FireWeapon() function. Add the TakeDamage implementation:
float ANovaCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// Let the parent class process the damage first
float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
// Subtract damage and clamp between 0 and max
CurrentHealth = FMath::Clamp(CurrentHealth - ActualDamage, 0.f, MaxHealth);
UE_LOG(LogTemp, Log, TEXT("Player health: %f"), CurrentHealth);
// Disable input when health reaches zero
if (CurrentHealth <= 0.f)
{
DisableInput(Cast<APlayerController>(GetController()));
}
return ActualDamage;
}
What does this code do?
Super::TakeDamage calls the parent class version so Unreal's internal bookkeeping still runs. The returned value is the actual damage dealt after any built-in modifiers.
FMath::Clamp keeps health between 0 and the maximum. When health drops to zero, DisableInput freezes the player controller so the character stops responding to input. This is a simple "game over" state.
- Save NovaCharacter.cpp.
- Compile the project. In the Unreal Editor Output Log, you should see a successful compile with no errors.
Seeing a compile error?
Check that the TakeDamage signature in your .cpp matches the declaration in your .h exactly. Even a missing const& on FDamageEvent will cause a mismatch.
Make sure your health properties are in the public section of the class. The HUD widget will need to read them from Blueprint later.
Help me fix my TakeDamage compile error in NovaCharacter
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the full NovaCharacter.h at this point:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "NovaCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
class ALaserWeapon;
UCLASS()
class NOVASTAR_API ANovaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ANovaCharacter();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
UCameraComponent* FollowCamera;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Health)
float CurrentHealth = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Health)
float MaxHealth = 100.f;
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
USpringArmComponent* CameraBoom;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputMappingContext* IMC_Nova;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Move;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Look;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Jump;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Sprint;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Dash;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* IA_Fire;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Weapon)
TSubclassOf<ALaserWeapon> LaserWeaponClass;
UPROPERTY()
ALaserWeapon* LaserWeapon;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartSprint();
void StopSprint();
void Dash();
void FireWeapon();
};
Here is the full NovaCharacter.cpp at this point:
#include "NovaCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "LaserWeapon.h"
ANovaCharacter::ANovaCharacter()
{
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom);
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->MaxWalkSpeed = 600.f;
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
}
void ANovaCharacter::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(IMC_Nova, 0);
}
}
if (LaserWeaponClass)
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
LaserWeapon = GetWorld()->SpawnActor<ALaserWeapon>(LaserWeaponClass, SpawnParams);
if (LaserWeapon)
{
LaserWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, TEXT("hand_rSocket"));
}
}
}
void ANovaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EIC->BindAction(IA_Move, ETriggerEvent::Triggered, this, &ANovaCharacter::Move);
EIC->BindAction(IA_Look, ETriggerEvent::Triggered, this, &ANovaCharacter::Look);
EIC->BindAction(IA_Jump, ETriggerEvent::Started, this, &ACharacter::Jump);
EIC->BindAction(IA_Jump, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EIC->BindAction(IA_Sprint, ETriggerEvent::Started, this, &ANovaCharacter::StartSprint);
EIC->BindAction(IA_Sprint, ETriggerEvent::Completed, this, &ANovaCharacter::StopSprint);
EIC->BindAction(IA_Dash, ETriggerEvent::Started, this, &ANovaCharacter::Dash);
EIC->BindAction(IA_Fire, ETriggerEvent::Triggered, this, &ANovaCharacter::FireWeapon);
}
}
void ANovaCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void ANovaCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
void ANovaCharacter::StartSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 1200.f;
}
void ANovaCharacter::StopSprint()
{
GetCharacterMovement()->MaxWalkSpeed = 600.f;
}
void ANovaCharacter::Dash()
{
FVector DashDirection = GetActorForwardVector();
LaunchCharacter(DashDirection * 1500.f, true, false);
}
void ANovaCharacter::FireWeapon()
{
if (LaserWeapon)
{
LaserWeapon->Fire();
}
}
float ANovaCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
CurrentHealth = FMath::Clamp(CurrentHealth - ActualDamage, 0.f, MaxHealth);
UE_LOG(LogTemp, Log, TEXT("Player health: %f"), CurrentHealth);
if (CurrentHealth <= 0.f)
{
DisableInput(Cast<APlayerController>(GetController()));
}
return ActualDamage;
}
Create the HUD widget
Your character can now take damage, but the player has no way to see their health. A HUD (heads-up display) solves this by overlaying information on the screen.
You will create a Widget Blueprint for the visuals, then a C++ GameMode class that spawns it when the game starts.
- In the Unreal Editor Content Browser, right-click in an empty area.
- Select User Interface, then Widget Blueprint.
- Name it WBP_NovaHUD.
- Double-click WBP_NovaHUD to open the Widget Designer.
- From the Palette panel, drag a Progress Bar onto the canvas. This will display health.
- In the Details panel for the Progress Bar, click Bind next to the Percent property.
- Create a binding that gets the player character, casts to NovaCharacter, and returns CurrentHealth / MaxHealth.
- Drag a Text Block onto the canvas for the ammo count.
- Set the Text Block font color to a bright cyan (R: 0, G: 255, B: 255).
- Set the widget's background to a dark transparent color so it is visible against any scene.
- Click Compile in the Widget Blueprint toolbar, then save.
What is UMG?
UMG (Unreal Motion Graphics) is Unreal's UI framework. Widget Blueprints let you visually design screen layouts. The binding you created makes the Progress Bar automatically poll the character's health every frame.
Now you need a C++ class that actually displays this widget when the game starts. A custom GameMode is the right place for this logic.
- In Unreal Editor, go to Tools > New C++ Class.
- Choose Game Mode Base as the parent class.
- Name it NovaGameMode and click Create Class.
- Once the files open in VS Code, replace the contents of NovaGameMode.h with this code:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "NovaGameMode.generated.h"
UCLASS()
class NOVASTAR_API ANovaGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ANovaGameMode();
protected:
virtual void BeginPlay() override;
// Widget class to spawn as the HUD
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = HUD)
TSubclassOf<UUserWidget> HUDWidgetClass;
// Reference to the spawned widget instance
UPROPERTY()
UUserWidget* HUDWidget;
};
What does this code do?
HUDWidgetClass is a TSubclassOf property. This means you can assign your WBP_NovaHUD widget from the editor without hardcoding a reference in C++.
HUDWidget stores a pointer to the live widget instance after it is created. This keeps the widget alive and lets you update it later.
- Save NovaGameMode.h.
- Now open NovaGameMode.cpp and replace its contents with this implementation:
#include "NovaGameMode.h"
#include "Blueprint/UserWidget.h"
ANovaGameMode::ANovaGameMode()
{
}
void ANovaGameMode::BeginPlay()
{
Super::BeginPlay();
// Create and display the HUD widget
if (HUDWidgetClass)
{
HUDWidget = CreateWidget<UUserWidget>(GetWorld(), HUDWidgetClass);
if (HUDWidget)
{
HUDWidget->AddToViewport();
}
}
}
What does this code do?
CreateWidget instantiates a UMG widget from the class you assign in the editor. AddToViewport renders it on the player's screen.
This logic runs in BeginPlay because the GameMode is the first thing Unreal initializes for a level. By spawning the HUD here, it appears the moment gameplay begins.
- Save NovaGameMode.cpp.
- Compile the project to confirm both new files build successfully.
Compile error in the GameMode files?
Verify the include path is exactly Blueprint/UserWidget.h in the .cpp file. A typo in the include will cause an unresolved symbol error.
Help me fix my NovaGameMode compile error
✔️ Awesome, I've got everything!
Great. Make sure both GameMode files are saved before continuing.
ⓧ I'd like to double check the full code
Here is the full NovaGameMode.h:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "NovaGameMode.generated.h"
UCLASS()
class NOVASTAR_API ANovaGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ANovaGameMode();
protected:
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = HUD)
TSubclassOf<UUserWidget> HUDWidgetClass;
UPROPERTY()
UUserWidget* HUDWidget;
};
Here is the full NovaGameMode.cpp:
#include "NovaGameMode.h"
#include "Blueprint/UserWidget.h"
ANovaGameMode::ANovaGameMode()
{
}
void ANovaGameMode::BeginPlay()
{
Super::BeginPlay();
if (HUDWidgetClass)
{
HUDWidget = CreateWidget<UUserWidget>(GetWorld(), HUDWidgetClass);
if (HUDWidget)
{
HUDWidget->AddToViewport();
}
}
}
Wire up the GameMode and test
The GameMode code exists, but Unreal does not know to use it yet. You need to tell the project which GameMode and which character to spawn by default.
- In Unreal Editor, go to Edit > Project Settings.
- In the left sidebar, select Maps & Modes (under the Project section).
- Set Default GameMode to NovaGameMode.
- Expand Default GameMode and set Default Pawn Class to NovaCharacter.
Now you need to tell the GameMode which widget to display.
- Open the World Settings panel (Window > World Settings).
- Confirm that GameMode Override shows NovaGameMode.
- In the Details panel for the GameMode, set HUD Widget Class to WBP_NovaHUD.
Why use a GameMode for the HUD?
The GameMode controls rules that apply to the entire game session. Spawning the HUD here means it appears regardless of which level you load. If you put it in the character class instead, you would need to handle widget cleanup on respawn.
- Click Play (or press Alt+P) to start the game in the editor.
- You should see the HUD appear on screen with the health Progress Bar filled to full.
- Walk your character off a ledge to take fall damage. Watch the Progress Bar decrease.
- Check the Output Log for a line that reads Player health: followed by a number less than 100. This confirms your TakeDamage function is working.
HUD not showing up?
Make sure you set HUD Widget Class to WBP_NovaHUD in the GameMode defaults. Without this assignment, HUDWidgetClass is null and the if guard skips widget creation.
Confirm that Default GameMode in Project Settings points to NovaGameMode and not the default template GameMode.
If the Progress Bar does not update on fall damage, verify the binding in your Widget Blueprint casts to NovaCharacter and reads CurrentHealth / MaxHealth.
Help me debug why my HUD widget is not appearing in Play mode
Your character now has a complete health system and a live HUD. Next up, you will create AI enemies that patrol the arena and chase the player using Behavior Trees.
Create AI Enemies with Behavior Trees
You have a character who can move, shoot, and take damage. But a combat arena is no fun without enemies to fight.
In this step, you will create AI enemies that patrol the arena and chase you when they spot you. The AI brain is powered by a Behavior Tree, Unreal's visual decision-making system for NPCs.
In this step, get ready to:
- Create a C++ enemy character class with health and a death response.
- Build a Behavior Tree and Blackboard that drives patrol and chase behaviors.
- Create an AI Controller that connects perception to decision-making and spawn enemies in the arena.
Create the enemy character
Every enemy needs its own character class to hold health, configure movement speed, and respond to damage. You will create a lightweight C++ class that destroys the enemy when its health reaches zero.
- In Unreal Editor, go to Tools > New C++ Class.
- Choose Character as the parent class.
- Name it EnemyCharacter and click Create Class.
The files open in VS Code. Replace the contents of EnemyCharacter.h with this header:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "EnemyCharacter.generated.h"
UCLASS()
class NOVASTAR_API AEnemyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AEnemyCharacter();
// Enemy health pool
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Health)
float Health = 100.f;
// Override to handle incoming damage
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
};
What does this code do?
This header defines a simple enemy with a single Health property starting at 100. The TakeDamage override lets the enemy respond when your laser hits it.
- Now replace the contents of EnemyCharacter.cpp with this implementation:
#include "EnemyCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
AEnemyCharacter::AEnemyCharacter()
{
// Enemies move slower than the player
GetCharacterMovement()->MaxWalkSpeed = 400.f;
}
float AEnemyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
Health = FMath::Clamp(Health - ActualDamage, 0.f, 100.f);
UE_LOG(LogTemp, Log, TEXT("Enemy health: %f"), Health);
if (Health <= 0.f)
{
Destroy();
}
return ActualDamage;
}
What does this code do?
The constructor sets MaxWalkSpeed to 400, making enemies noticeably slower than your player (600). This gives you a tactical advantage when kiting them around the arena.
The TakeDamage function subtracts damage from health and calls Destroy() when health hits zero. The enemy simply disappears from the world.
- Save both files.
- Compile by pressing Ctrl+Shift+B in VS Code, or use Live Coding in the Unreal Editor.
Now create a Blueprint from this class so you can assign a visible mesh to the enemy.
- In the Content Browser, right-click and select Blueprint Class.
- In the All Classes dropdown, search for EnemyCharacter and select it as the parent.
- Name the Blueprint BP_EnemyCharacter.
- Double-click to open it. In the Components panel, select the Mesh component.
- In the Details panel under Mesh, set the Skeletal Mesh Asset to the Mannequin mesh (SKM_Manny or SKM_Quinn).
- Click Compile and Save in the Blueprint editor.
Don't see EnemyCharacter in the class list?
Make sure you compiled successfully before creating the Blueprint. Check the Output Log in the editor for errors. If you see red errors about missing includes, ensure GameFramework/CharacterMovementComponent.h is included in EnemyCharacter.cpp.
Help me fix my EnemyCharacter compilation errors.
Set up the Behavior Tree
A Blackboard is a shared memory board where AI stores facts about the world. The Behavior Tree reads these facts to decide what to do next.
- In the Content Browser, right-click and select Artificial Intelligence > Blackboard.
- Name it BB_Enemy.
- Double-click to open it. Click New Key and add a Vector key named PlayerLocation.
- Click New Key again and add a Bool key named CanSeePlayer.
- Save the Blackboard.
What is a Blackboard?
Think of a Blackboard as the enemy's short-term memory. When the AI Controller spots the player, it writes CanSeePlayer = true and the player's position to the Blackboard. The Behavior Tree reads those values each tick to decide whether to chase or patrol.
Now create the Behavior Tree that reads these Blackboard values.
- In the Content Browser, right-click and select Artificial Intelligence > Behavior Tree.
- Name it BT_Enemy.
- Double-click to open it. In the Details panel of the Behavior Tree, set the Blackboard Asset to BB_Enemy.
Build the tree structure by adding nodes from the root.
- Right-click the Root node and add a Selector composite.
- Right-click the Selector and add a Sequence composite. This is the "chase" branch.
- Right-click the Sequence and add a Decorator > Blackboard. Set Blackboard Key to CanSeePlayer and Key Value to Is Set.
- Under that Sequence, add a Move To task. Set its Blackboard Key to PlayerLocation.
- Right-click the Selector again and add a second Sequence to the right. This is the "patrol" branch.
- Under the patrol Sequence, add a task. Right-click and choose Tasks > a random movement task such as a custom BTTask_MoveToRandomLocation or use an EQS query that generates a reachable point within a radius. The simplest approach is to create a Blueprint Task that calls GetRandomReachablePointInRadius and writes the result to a Blackboard vector key, then follow it with a Move To task targeting that key.
- Save the Behavior Tree.
How does the Selector decide?
A Selector tries its children left to right and stops at the first one that succeeds. The left branch (chase) only runs when the CanSeePlayer decorator passes. If the enemy has lost sight, that branch fails and the Selector falls through to the patrol branch on the right.
Behavior Tree not linking to the Blackboard?
Double-check that you set the Blackboard Asset property on the Behavior Tree itself (in the tree's own Details panel, not on a node). Without this link, the tree cannot read any keys.
Help me connect my Behavior Tree to the Blackboard in Unreal Engine 5.
Create the AI Controller and spawn enemies
The AI Controller is the brain that possesses the enemy pawn and runs the Behavior Tree. It also holds the perception system that detects the player.
- In Unreal Editor, go to Tools > New C++ Class.
- Click Show All Classes, search for AIController, select it as the parent.
- Name the class EnemyAIController and click Create Class.
Replace the contents of EnemyAIController.h with this header:
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionComponent.h"
#include "EnemyAIController.generated.h"
class UBehaviorTree;
UCLASS()
class NOVASTAR_API AEnemyAIController : public AAIController
{
GENERATED_BODY()
public:
AEnemyAIController();
// Assign BT_Enemy in the Blueprint
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = AI)
UBehaviorTree* BehaviorTree;
protected:
virtual void OnPossess(APawn* InPawn) override;
// Perception component detects the player by sight
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = AI)
UAIPerceptionComponent* AIPerception;
// Called when the perception state changes
UFUNCTION()
void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
};
What does this code do?
The header declares three key pieces. BehaviorTree is a reference you will assign in the editor to BT_Enemy. AIPerception is the component that gives the enemy eyes. OnPerceptionUpdated fires whenever the enemy spots or loses sight of something.
- Now replace the contents of EnemyAIController.cpp with this implementation:
#include "EnemyAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionComponent.h"
AEnemyAIController::AEnemyAIController()
{
// Create and configure the perception component
AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
UAISenseConfig_Sight* SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
SightConfig->SightRadius = 1500.f;
SightConfig->LoseSightRadius = 2000.f;
SightConfig->PeripheralVisionAngleDegrees = 60.f;
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
AIPerception->ConfigureSense(*SightConfig);
AIPerception->SetDominantSense(SightConfig->GetSenseImplementation());
AIPerception->OnTargetPerceptionUpdated.AddDynamic(this, &AEnemyAIController::OnPerceptionUpdated);
}
void AEnemyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (BehaviorTree)
{
RunBehaviorTree(BehaviorTree);
}
}
void AEnemyAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (Blackboard)
{
Blackboard->SetValueAsBool(TEXT("CanSeePlayer"), Stimulus.WasSuccessfullySensed());
if (Stimulus.WasSuccessfullySensed())
{
Blackboard->SetValueAsVector(TEXT("PlayerLocation"), Actor->GetActorLocation());
}
}
}
What does this code do?
The constructor configures a sight sense with a 1500-unit detection radius and 60-degree field of view. When the enemy spots the player, OnPerceptionUpdated fires and writes CanSeePlayer and PlayerLocation to the Blackboard.
The OnPossess override starts the Behavior Tree as soon as the controller takes control of an enemy pawn. This connects the brain to the body.
- Save both files and compile.
Now assign the AI Controller to your enemy Blueprint.
- Open BP_EnemyCharacter in the Blueprint editor.
- In the Details panel, find the Pawn section.
- Set AI Controller Class to EnemyAIController.
- Compile and save the Blueprint.
Finally, update your GameMode to spawn enemies into the arena. Open NovaGameMode.h and find this section:
UPROPERTY()
UUserWidget* HUDWidget;
- Add the following properties below that line:
// Blueprint class to spawn as enemies
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Enemies)
TSubclassOf<ACharacter> EnemyClass;
// World locations where enemies spawn
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Enemies)
TArray<FVector> EnemySpawnLocations;
What does this code do?
These two properties let you configure enemy spawning from the editor. EnemyClass is the Blueprint to spawn (BP_EnemyCharacter). EnemySpawnLocations is an array of world positions you can set in the GameMode Blueprint's details.
- Now open NovaGameMode.cpp. Find this include at the top:
#include "Blueprint/UserWidget.h"
- Add this include directly below it:
#include "EnemyCharacter.h"
- In the BeginPlay function, find the closing brace of the if (HUDWidget) block. Add this spawn loop after it, before the function's closing brace:
// Spawn enemies at each predefined location
for (const FVector& SpawnLocation : EnemySpawnLocations)
{
FActorSpawnParameters SpawnParams;
GetWorld()->SpawnActor<ACharacter>(EnemyClass, SpawnLocation, FRotator::ZeroRotator, SpawnParams);
}
What does this code do?
This loop iterates over every position in EnemySpawnLocations and spawns an instance of EnemyClass there. Each spawned enemy automatically gets possessed by the AI Controller you assigned in the Blueprint.
- Save both GameMode files and compile.
- In the editor, open your level's World Settings and confirm NovaGameMode is selected.
- In the GameMode override's Details panel, set Enemy Class to BP_EnemyCharacter.
- Add 3 to 5 entries in the Enemy Spawn Locations array with positions spread around your level.
- Press Alt+P to play. You should see enemies appear at the spawn locations, patrol around, and chase you when you enter their line of sight.
Enemies not chasing you?
Open BP_EnemyCharacter and verify the AI Controller Class is set to EnemyAIController. Also confirm you assigned BT_Enemy to the Behavior Tree property on the AI Controller Blueprint defaults.
If enemies spawn but stand still, enable AI debugging by pressing the apostrophe (') key during Play to view the Behavior Tree state live.
Help me debug why my AI enemies aren't responding to seeing the player.
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files before moving on.
ⓧ I'd like to double check the full code
Here is the full code for each file modified in this step.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "EnemyCharacter.generated.h"
UCLASS()
class NOVASTAR_API AEnemyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AEnemyCharacter();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Health)
float Health = 100.f;
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
};
#include "EnemyCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
AEnemyCharacter::AEnemyCharacter()
{
GetCharacterMovement()->MaxWalkSpeed = 400.f;
}
float AEnemyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
Health = FMath::Clamp(Health - ActualDamage, 0.f, 100.f);
UE_LOG(LogTemp, Log, TEXT("Enemy health: %f"), Health);
if (Health <= 0.f)
{
Destroy();
}
return ActualDamage;
}
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionComponent.h"
#include "EnemyAIController.generated.h"
class UBehaviorTree;
UCLASS()
class NOVASTAR_API AEnemyAIController : public AAIController
{
GENERATED_BODY()
public:
AEnemyAIController();
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = AI)
UBehaviorTree* BehaviorTree;
protected:
virtual void OnPossess(APawn* InPawn) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = AI)
UAIPerceptionComponent* AIPerception;
UFUNCTION()
void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
};
#include "EnemyAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionComponent.h"
AEnemyAIController::AEnemyAIController()
{
AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
UAISenseConfig_Sight* SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
SightConfig->SightRadius = 1500.f;
SightConfig->LoseSightRadius = 2000.f;
SightConfig->PeripheralVisionAngleDegrees = 60.f;
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
AIPerception->ConfigureSense(*SightConfig);
AIPerception->SetDominantSense(SightConfig->GetSenseImplementation());
AIPerception->OnTargetPerceptionUpdated.AddDynamic(this, &AEnemyAIController::OnPerceptionUpdated);
}
void AEnemyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (BehaviorTree)
{
RunBehaviorTree(BehaviorTree);
}
}
void AEnemyAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (Blackboard)
{
Blackboard->SetValueAsBool(TEXT("CanSeePlayer"), Stimulus.WasSuccessfullySensed());
if (Stimulus.WasSuccessfullySensed())
{
Blackboard->SetValueAsVector(TEXT("PlayerLocation"), Actor->GetActorLocation());
}
}
}
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "NovaGameMode.generated.h"
UCLASS()
class NOVASTAR_API ANovaGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ANovaGameMode();
protected:
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = HUD)
TSubclassOf<UUserWidget> HUDWidgetClass;
UPROPERTY()
UUserWidget* HUDWidget;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Enemies)
TSubclassOf<ACharacter> EnemyClass;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Enemies)
TArray<FVector> EnemySpawnLocations;
};
#include "NovaGameMode.h"
#include "Blueprint/UserWidget.h"
#include "EnemyCharacter.h"
ANovaGameMode::ANovaGameMode()
{
}
void ANovaGameMode::BeginPlay()
{
Super::BeginPlay();
if (HUDWidgetClass)
{
HUDWidget = CreateWidget<UUserWidget>(GetWorld(), HUDWidgetClass);
if (HUDWidget)
{
HUDWidget->AddToViewport();
}
}
for (const FVector& SpawnLocation : EnemySpawnLocations)
{
FActorSpawnParameters SpawnParams;
GetWorld()->SpawnActor<ACharacter>(EnemyClass, SpawnLocation, FRotator::ZeroRotator, SpawnParams);
}
}
Your arena now has enemies with brains. Next up, you will build the sci-fi arena itself with lighting, atmosphere, and a full playtest to tie everything together.
Build the Sci-Fi Arena and Playtest
Your character can move, shoot, take damage, and the AI enemies patrol and chase. All the gameplay systems are wired up and working.
Now you need a space for everything to come together. In this step, you will build an enclosed sci-fi arena, add atmospheric lighting and post-processing, then run a full playtest to confirm every system works end-to-end.
In this step, get ready to:
- Build an enclosed arena with walls, ramps, and platforms using Starter Content meshes.
- Add sci-fi atmosphere with coloured lights and post-processing effects.
- Place spawn points and run a full playtest of all gameplay systems.
Build the arena
The default Third Person template comes with pre-built geometry that does not fit your sci-fi theme. You will clear it out and construct your own enclosed combat arena from scratch using the meshes included in Starter Content.
- In the Outliner panel (top-right of the Unreal Editor), select all the existing geometry actors in the level (floors, walls, ramps from the template).
- Press Delete to remove them.
- Leave the Sky Sphere, Atmospheric Fog, and Player Start if they exist. You will reposition the Player Start later.
Now build the arena floor and walls.
- Open the Content Browser at the bottom of the editor.
- Navigate to Content > StarterContent > Architecture.
- Drag a large floor mesh (such as Floor_400x400) into the viewport. Duplicate it (Alt+Drag) to tile out a square arena roughly 4x4 tiles.
- Drag wall meshes (such as Wall_400x300) and position them around all four edges to enclose the arena.
- Add 2-3 ramp meshes leading up to elevated platforms. Stack floor tiles at a higher Z position to create the platforms.
- Use the W key (translate), E key (rotate), and R key (scale) to position meshes precisely.
Use grid snapping for clean placement
Hold Ctrl while dragging to snap to the grid. This keeps your walls aligned perfectly with the floor tiles without gaps.
Now add lighting to the arena.
- In the top menu, click Edit > Place Actors (or press Ctrl+Shift+1) to open the Place Actors panel.
- Search for Point Light and drag 4-6 point lights into the arena, spacing them evenly.
- Select each point light. In the Details panel, click the Light Color swatch and set it to a cyan or blue tone.
- Set Intensity to around 5000 for each point light so they cast a visible glow.
- Drag a Directional Light into the level from the Place Actors panel. This provides overall illumination so the arena is not pitch black between point lights.
Why both point lights and a directional light?
Point lights create localised pools of coloured light that sell the sci-fi mood. The directional light acts like a sun or overhead floodlight, filling in the shadows so the player can always see where they are going.
Your arena should now look like an enclosed space with blue-tinted lighting. Walls surround the play area, ramps lead to elevated platforms, and coloured lights give it a sci-fi feel.
Add post-processing and atmosphere
Post-processing effects add a cinematic layer on top of what the camera sees. A single volume can transform a grey box level into something that feels like a polished sci-fi environment.
- In the Place Actors panel, search for Post Process Volume and drag one into the level.
- Select the Post Process Volume in the Outliner.
- In the Details panel, scroll to the Post Process Volume Settings section and check Infinite Extent (Unbound). This makes the volume affect the entire level regardless of its physical size.
Now configure the visual effects.
- In the Details panel, expand the Bloom section.
- Set Intensity to 0.5. This adds a subtle glow around bright surfaces without washing out the image.
- Expand the Color Grading section, then expand Global.
- Click the Gain colour bar and shift it toward a cool blue tone. This tints the entire scene to match your sci-fi palette.
- Expand the Exposure section. Confirm that Metering Mode is set to Auto Exposure Histogram (the default). This automatically adjusts brightness as the player moves between dark and bright areas.
What does the Post Process Volume do?
The Post Process Volume applies screen-space effects after the scene is rendered. Bloom makes bright surfaces glow. Color grading shifts the overall colour palette. Auto exposure simulates how a camera adapts to different light levels.
Together these effects give your grey-box arena a polished sci-fi atmosphere without needing any custom art assets.
Your viewport should now show the arena with a cool blue tint and subtle glow on bright surfaces.
Place actors and run a full playtest
The arena is built and lit. Now you need to place the Player Start, configure enemy spawn locations in your GameMode, and run a full playtest to confirm every system works together.
- In the Outliner, find the existing Player Start actor (or drag a new one from the Place Actors panel if you deleted it earlier).
- Position it on the arena floor in a corner, slightly away from walls so the player does not spawn clipping into geometry.
- Note down 4-6 positions around the arena where you want enemies to spawn. Pick varied locations: some on the ground floor, some on elevated platforms.
Now configure those spawn positions in your GameMode.
- In the World Settings panel (accessible via Window > World Settings), confirm the GameMode Override is set to your NovaGameMode class.
- Click the GameMode Override dropdown, then click the blue link to open the GameMode's Details panel.
- Find the Enemy Spawn Locations array property. Click the + button 4-6 times to add entries.
- For each entry, type the X, Y, Z coordinates of your chosen spawn positions. A simple layout would place enemies at the four corners of the arena plus two on elevated platforms.
- Set Enemy Class to your BP_EnemyCharacter Blueprint.
Finding coordinates quickly
Place a temporary actor (like a cube) at each desired spawn point, then read its Location values from the Details panel's Transform section. Copy those X, Y, Z values into your EnemySpawnLocations array, then delete the temporary actors.
Time to playtest. Press Alt+P to start Play In Editor mode.
- Test movement: W/A/S/D to move, mouse to look around.
- Test sprint by holding Left Shift. Your character should move noticeably faster.
- Test dash by pressing E. Your character should launch forward.
- Test jump by pressing Space. Navigate up a ramp to confirm elevated platforms are reachable.
- Test the laser weapon by clicking Left Mouse Button. You should see red debug lines on hit and green lines on miss.
- Shoot an enemy. Watch the Output Log (Window > Developer Tools > Output Log) for health messages. After enough hits, the enemy should be destroyed.
- Observe the AI enemies. They should patrol around until they spot you, then chase toward your position.
- Check the HUD. The health bar should be full at the start. If an enemy can damage you (or you take fall damage), confirm the bar decreases.
- Press Escape to stop the play session when done.
What to adjust for better gameplay feel
If the arena feels too cramped, scale up the floor and move walls outward. If enemies die too fast, increase their Health value in the BP_EnemyCharacter Details panel. If movement feels sluggish, increase MaxWalkSpeed in your character's constructor.
Game development is iterative. Tweak one value at a time, playtest, and repeat until the combat feels satisfying.
Enemies not spawning?
Check that Enemy Class is set to BP_EnemyCharacter in your GameMode (not the base C++ class). Make sure the Enemy Spawn Locations array has at least one entry, and that the Z values are above the arena floor so enemies do not spawn inside geometry.
Also confirm that GameMode Override in World Settings points to your NovaGameMode and not the engine default.
Help me figure out why my enemies aren't spawning in my UE5 arena.
Your sci-fi combat arena is complete. You have a playable prototype with movement, weapons, AI enemies, and atmosphere all working together.
Secret mission
Add a Plasma Grenade Projectile
Your laser weapon is great for single targets, but what about clusters of enemies? In this secret mission, you will build a physics-based plasma grenade that arcs through the air, explodes on impact, and deals area damage to everything nearby.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. There are no cloud services or ongoing costs to worry about.
Decide whether to keep your resources on disk, close the editor to free up memory, or delete everything to reclaim space.
Resources you used:
- NovaStar project folder (5-20 GB depending on assets and intermediate files).
- Unreal Engine 5 installation (40-60 GB).
- Epic Games Launcher.
- Build Tools for Visual Studio 2022 (Windows) or Clang (Linux).
Keep everything running
No action needed. Choose this if you plan to keep building on your sci-fi arena or want to start new Unreal Engine projects.
- Your NovaStar project folder stays on your Desktop.
- Unreal Engine and the Epic Games Launcher remain installed and ready for your next project.
- Your compiler toolset (Build Tools for Visual Studio 2022 or Clang) stays available for any C++ development.
- The VS Code extensions (C/C++ Extension Pack and C# extension) remain installed and are useful for other projects.
Pause - I'll come back to this later
Close Unreal Editor to free up system memory while keeping all your files intact.
- Close the Unreal Editor window for the NovaStar project.
- Close the Epic Games Launcher from the system tray to free additional memory.
- Your project folder and all installed tools remain on disk. You can reopen the project at any time by double-clicking the NovaStar.uproject file.
Delete - I don't want to use this again
Remove all project resources to reclaim disk space. Unreal Engine projects and the engine itself can take up 50-80 GB combined.
Delete the NovaStar project folder
- Delete the project folder by running the command for your platform:
macOS
rm -rf ~/Desktop/NovaStar
Windows
Remove-Item -Path "$env:USERPROFILE\Desktop\NovaStar" -Recurse -Force
Linux
rm -rf ~/Desktop/NovaStar
Uninstall Unreal Engine
- Open the Epic Games Launcher.
- Click the Unreal Engine tab on the left sidebar.
- Go to Library.
- Click the dropdown menu on your installed engine version and select Remove.
- Click Uninstall in the dialog to confirm.
Uninstall the Epic Games Launcher
macOS
- Open Finder and go to the Applications folder.
- Find the Epic Games Launcher app.
- Right-click and select Move to Trash.
- Empty the Trash to permanently remove the launcher.
Windows
- Press the Windows key and type Add or Remove Programs.
- Press Enter to open the settings page.
- Find Epic Games Launcher in the list of apps and click Uninstall.
- Confirm the uninstallation when prompted.
Linux
Delete the Epic Games Launcher installation directory and its config files.
- Navigate to the directory where you installed the Epic Games Launcher and delete it.
- Remove associated config files by running:
rm -rf ~/.config/Epic
Uninstall the compiler toolset
macOS
This project's compiler setup targeted Windows and Linux. If you did not install a project-specific compiler on macOS, no action is needed here.
Windows
- Press the Windows key and type Add or Remove Programs.
- Press Enter to open the settings page.
- Find Visual Studio Build Tools 2022 in the list of apps and click Uninstall.
- Confirm the uninstallation when prompted.
Linux
Remove Clang by running:
sudo apt remove clang
That's a Wrap!
That's a Wrap!
Nice work! You just built a playable sci-fi combat arena from scratch using Unreal Engine 5 and C++. That is no small feat.
You've learned how to:
- Created a custom C++ Character class with Enhanced Input for responsive third-person movement, sprint, and dash.
- Built a laser weapon system using line traces and Unreal's damage framework to shoot and destroy targets.
- Created AI enemies with Behavior Trees that patrol the arena and chase the player on sight.
- Added a physics-based plasma grenade with radial damage that explodes on impact.
Ready to quiz yourself?