quarta-feira, 20 de janeiro de 2021

Using Line Traces in C++

Line Traces are used to test for collision using a defined line segment. There are several types of Line Traces. A Line Trace can be based on channel (Visibility or Camera) or on the type of the object (WorldStatic, WorldDynamic, Pawn, ...). 

Line Trace functions are defined in the UWorld class. There are functions that return only the first object found and other functions that return multiple objects that have collided with the line segment.

In this article, we will focus on one of the Line Trace functions. The function is LineTraceSingleByChannel() which is channel-based and returns the first object found. The function call in the example code looks like this:

bool bHitSomething = GetWorld()->LineTraceSingleByChannel(OutHitResult,
                                                          StartLocation,
                                                          EndLocation,
                                                          ECC_Visibility,
                                                          LineTraceParams);

Let's look at each part of this function call:

  • bHitSomethingBoolean variable that stores the function's return value. Receives true if the Line Trace collided with an object.
  • GetWorld()->LineTraceSingleByChannel()The GetWorld() function returns a pointer of type UWorld that represents the current level. The Line Trace function is called using this pointer.
  • OutHitResultVariable of type FHitResult. It will be filled with various information related to the object found.
  • StartLocationFVector that defines the beginning of the line segment used in the collision test.
  • EndLocationFVector that defines the end of the line segment.
  • ECC_VisibilityThis constant indicates that the Visibility channel will be used for the collision test.
  • LineTraceParamsVariable of type FCollisionQueryParams that can be filled with parameter values that will be passed to the Line Trace function. 


Example usage:

For this example, you will need a C++ project that has the First Person template with Starter Content. I created a project with the name TutoFirstPerson.

Let's modify the Player Character to use Line Trace to interact with a wall sconce. The Player Character must be looking in the direction of the wall sconce which must be less than 3 meters away. When pressing the E key, the wall sconce light will turn on or off.

Create a C++ class with the name WallSconce using the Actor class as the parent class. This class contains a StaticMeshComponent and a PointLightComponent. It also has the PressSwitch() function that will be used by the player to turn the light on or off. The WallSconce.h file has this content:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "WallSconce.generated.h"

UCLASS()
class AWallSconce : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AWallSconce();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(VisibleAnywhere)
	USceneComponent* RootScene;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* StaticMeshComponent;
	
	UPROPERTY(VisibleAnywhere)	
	class UPointLightComponent* PointLightComponent;
	
	void PressSwitch();
};

The WallSconce class uses the Static Mesh SM_Lamp_Wall. In the constructor, we have the configuration of the Static Mesh and Point Light components. The PressSwitch() function calls PointLightComponent's ToggleVisibility() function. This is the contents of the WallSconce.cpp file:

#include "WallSconce.h"
#include "Components/PointLightComponent.h"

// Sets default values
AWallSconce::AWallSconce()
{
  // Set this actor to call Tick() every frame.
  PrimaryActorTick.bCanEverTick = true;

  RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
  RootComponent = RootScene;

  StaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(
                                              TEXT("StaticMeshComponent"));
  StaticMeshComponent->SetupAttachment(RootScene);
  
  ConstructorHelpers::FObjectFinder<UStaticMesh> MeshFile(
    TEXT("/Game/StarterContent/Props/SM_Lamp_Wall.SM_Lamp_Wall"));

  if (MeshFile.Succeeded())
  {
    StaticMeshComponent->SetStaticMesh(MeshFile.Object);
  }
  
  // PointLightComponent initialization
  PointLightComponent = CreateDefaultSubobject<UPointLightComponent>(
                                              TEXT("PointLightComponent"));
  PointLightComponent->SetIntensity(1000.f);
  PointLightComponent->SetLightColor(FLinearColor(1.f, 1.f, 1.f));
  PointLightComponent->SetupAttachment(RootScene);
  PointLightComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 30.0f));
}

void AWallSconce::PressSwitch()
{
  PointLightComponent->ToggleVisibility();
}

// Called when the game starts or when spawned
void AWallSconce::BeginPlay()
{
  Super::BeginPlay();	
}

// Called every frame
void AWallSconce::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);
}

Now we are going to create an input mapping called Interact that will be triggered when the player presses the E key.

In the level editor, access the Edit->Project Settings... menu and in the Engine category choose the Input option. Click the + symbol next to Action Mappings, enter the name Interact for the new Action Mapping, and select the E key. The Action Mappings for the project will look like this:


Open the header file of the Character class created by the First Person template. The file name in my example project is TutoFirstPersonCharacter.h. Add the declaration of the InteractWithWorld() function below the SetupPlayerInputComponent() function:

  // APawn interface
  virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) 
                                                                 override;
  // End of APawn interface

  void InteractWithWorld();

In the Character cpp file, you must add the #include at the beginning of the file:

#include "WallSconce.h"

In the SetupPlayerInputComponent() function, the Interact Input will be bound with the InteractWithWorld() function. Change the name ATutoFirstPersonCharacter to the name of your project's Character class and don't forget to put the & operator before the name. It is used to return the memory address of the function. 

PlayerInputComponent->BindAction("Interact", IE_Pressed, this,
                                 &ATutoFirstPersonCharacter::InteractWithWorld);

At the end of the cpp file, create the InteractWithWorld() function with the code below. Don't forget to change the name of the class that is before the "::" operator.

void ATutoFirstPersonCharacter::InteractWithWorld()
{
  float LengthOfTrace = 300.f;
	
  FVector StartLocation;
  FVector EndLocation;
  
  StartLocation = FirstPersonCameraComponent->GetComponentLocation();
  
  EndLocation = StartLocation + 
    (FirstPersonCameraComponent->GetForwardVector() * LengthOfTrace);
	
  FHitResult OutHitResult;
  FCollisionQueryParams LineTraceParams;  
   
  bool bHitSomething = GetWorld()->LineTraceSingleByChannel(OutHitResult, 
                       StartLocation, EndLocation, ECC_Visibility, LineTraceParams);

  if(bHitSomething)
  {
    
    AWallSconce* WallSconceInstance = Cast<AWallSconce>( OutHitResult.GetActor() );
	
    if(WallSconceInstance)
    {
      WallSconceInstance->PressSwitch();
    }
  }
}

A Cast<AWallSconce> was done to check if the Actor found by the Line Trace is of the AWallSconce class. If so, then the PressSwitch() function of the AWallSconce class will be called.

Compile the C ++ code. Place an instance of WallSconce on one of the walls. Start the game, go to the WallSconce and look towards it. Press the E key to turn the light on and off.



Table of Contents C++