top of page

Building Dark Souls-Inspired Combat Mechanics in Unreal Engine with C++

This mechanic is implemented to make it easy for both programmers and non-programmers to follow and implement on their own, as well as to ensure easy access within Blueprints.


Part 1 : Implementing Lock-On System

This system allows the player to lock onto a nearby target, such as an enemy, to ensure that the camera and character focus on them. This feature is essential for games that involve combat mechanics, providing better control and accuracy for the player.


Step 1: Creating 'ULockOnComponent' class

The ULockOnComponent class is a custom component that inherits from Unreal Engine's UActorComponent. The goal of this component is to handle the logic required to lock the camera onto a target and control the player's rotation towards that target.


NOTE : In Unreal Engine, composition means breaking down behaviors into separate, reusable components that are attached to an actor. Instead of putting all functionality in one class, you add specific components like a StaticMeshComponent for visuals, a MovementComponent for movement, or a HealthComponent for health. These components work together to give the actor its full functionality. This approach makes it easy to reuse, maintain, and extend behaviors across multiple actors without duplicating code.

Step 2:Locking Onto a Target: ;StartLockOn; function


The StartLockOn function is responsible for detecting and locking onto a target within a given radius.

This code performs a spherical trace (also known as a sweep) in Unreal Engine, which is used to detect nearby actors (such as enemies) within a specific radius.

  • FHitResult OutResult:FHitResult is a structure that holds the details (like actor and location) of any object hit during a trace or collision.

  • FVector CurrentLocation{ OwnerRef->GetActorLocation() }:This stores the current world location of the actor that owns this component (OwnerRef). GetActorLocation() retrieves the actor's position in the world.

  • FCollisionShape Sphere { FCollisionShape::MakeSphere(Radius) }:This creates a sphere-shaped collision object with a specified Radius. It's used to detect actors within this radius during a trace.

  • FCollisionQueryParams IgnoreParams{ FName(TEXT("Ignore Collision Params")), false, OwnerRef }:This sets up parameters for the trace, including ignoring the owning actor (OwnerRef) to prevent it from colliding with itself. It uses simple collision and a name for debugging.

  • bool bHasFoundTarget { GetWorld()->SweepSingleByChannel(...) }:This line performs a spherical trace from CurrentLocation using the specified collision channel, sphere shape, and ignore parameters. It stores the result in bHasFoundTarget, which will be true if a target is found.

  • if (!bHasFoundTarget) { return; }:If no target is detected (bHasFoundTarget is false), the function exits early, skipping any further processing.


NOTE : In Unreal Engine, tracing refers to detecting objects in the game world along a line or within a shape (like a raycast or box trace). A channel is a category assigned to a physics body that defines how it interacts with traces or collisions.

How Channels Work:

Each object (or actor) in the game can be assigned to a specific collision channel. When performing a trace, you specify which channels to check for collisions or overlaps, and only objects belonging to those channels will be detected.

Example Channels:

  • WorldStatic: Used for static, non-moving objects like walls, floors, or terrain.

  • Pawn: Used for characters or player-controlled objects like the player or NPCs.

  • Vehicle: Used for vehicles in the game.

Example Use Case:

If you perform a ray trace to check if there's a wall between the player and a target, you would specify the WorldStatic channel. The trace will only interact with objects in that channel, ignoring others like Pawn (characters) or Vehicle. This ensures you're only detecting walls or floors, and not characters or vehicles.

Channels help optimize traces by focusing on relevant object types, improving performance and precision in interactions.


Step 2: Locking the Camera and the movement


	OwnerRef = GetOwner<ACharacter>();
	Controller = GetWorld()->GetFirstPlayerController();
	MovementComponent = OwnerRef->GetCharacterMovement();

	SpringArmComp = OwnerRef->FindComponentByClass<USpringArmComponent();
	
	Controller->SetIgnoreLookInput(true);
	MovementComponent->bOrientRotationToMovement = false;
	MovementComponent->bUseControllerDesiredRotation = true;

	SpringArmComp->TargetOffset = FVector { 0.f,0.f,100.f};
  • OwnerRef = GetOwner<ACharacter>();: Gets the character that owns this component.

  • Controller = GetWorld()->GetFirstPlayerController();: Retrieves the player's controller.

  • MovementComponent = OwnerRef->GetCharacterMovement();: Accesses the character's movement system.

  • SpringArmComp = OwnerRef->FindComponentByClass<USpringArmComponent>();: Finds the spring arm component attached to the character (used for controlling the camera).

  • Controller->SetIgnoreLookInput(true);: Disables player control over camera rotation.

  • MovementComponent->bOrientRotationToMovement = false;: Stops character from rotating based on movement direction.

  • MovementComponent->bUseControllerDesiredRotation = true;: Makes the character rotate based on the controller's input.

  • SpringArmComp->TargetOffset = FVector { 0.f,0.f,100.f };: Adjusts the camera's position relative to the character by setting an offset.


Step 3 : Rotating the player towards the target


	if(!IsValid(CurrentTargetActor)) { return; }
	FVector CurrentLocation { OwnerRef->GetActorLocation() };
	FVector TargetLocation { CurrentTargetActor->GetActorLocation() };
	double TargetDistance {
		FVector::Distance(CurrentLocation, TargetLocation)
	};
	if(TargetDistance >= BreakDistance)
	{
		EndLockOn();
		return;
	}
	TargetLocation.Z -= 125;
	FRotator NewRotation { 			      		UKismetMathLibrary::FindLookAtRotation(CurrentLocation,TargetLocation) } ;
	Controller->SetControlRotation(NewRotation);

This code helps the player stay locked onto the target, adjusting the character or camera to continuously face the target until the distance exceeds a certain limit.


  • if(!IsValid(CurrentTargetActor)) { return; }: Checks if the target actor is valid; if not, the function exits.

  • FVector CurrentLocation { OwnerRef->GetActorLocation() };: Gets the current location of the owner (the player or character).

  • FVector TargetLocation { CurrentTargetActor->GetActorLocation() };: Retrieves the target actor's location.

  • double TargetDistance { FVector::Distance(CurrentLocation, TargetLocation) };: Calculates the distance between the player and the target.

  • if(TargetDistance >= BreakDistance) { EndLockOn(); return; }: If the distance is greater than the set BreakDistance, it ends the lock-on and exits the function.

  • TargetLocation.Z -= 125;: Adjusts the target's location by lowering its Z-axis (height) by 125 units, likely to offset aiming height.

  • FRotator NewRotation { UKismetMathLibrary::FindLookAtRotation(CurrentLocation, TargetLocation) };: Calculates the rotation needed for the player to face the target.

  • Controller->SetControlRotation(NewRotation);: Sets the player's camera or character rotation to face the target.



    Part 2 : Player Combat

UTraceComponent is an actor component that helps trace weapon swings, using sockets on skeletal meshes to determine start and end points for the weapon's path. By implementing this component, you can create dynamic combat interactions that respond accurately to the player's actions.


Step 1:

  • SkeletalComp: A pointer to the skeletal mesh component that the trace component interacts with. It’s initialized when the game starts.

  • Start, End, Rotation: These are FName properties referring to the sockets on the skeletal mesh, defining the start and end points of the trace as well as its orientation.

  • BoxCollisionLength: This property determines the dimensions of the collision shape (box) used to detect hits along the weapon's path.

  • bDebugMode: A boolean flag that toggles visual debug information, helping developers see the collision box in real time during development.


This method is called every frame and is where the core logic for tracing and collision detection occurs. It calculates the start and end positions based on the mesh sockets, defines a collision box, and performs a sweep to detect hits along the weapon's path.

  • Box Extent: The collision box is defined based on the distance between the start and end points of the weapon swing.

  • SweepMultiByChannel: This function checks if any objects within the specified channel are hit by the weapon, storing the results in OutResults.

  • Debug Visualization: If bDebugMode is enabled, a debug box is drawn, visually representing the collision path and whether a target was hit.

Comments


bottom of page