quarta-feira, 20 de janeiro de 2021

Usando Line Traces em C++

Line Traces são usados para testar se existe colisão usando um segmento de reta definido. Existem vários tipos de Line Traces. Um Line Trace pode ser baseado em channel (Visibility ou Camera) ou pelo tipo do objeto (WorldStatic, WorldDynamic, Pawn, etc.). 

As funções de Line Traces estão definidas na classe UWorld. Existem funções que retornam apenas o primeiro objeto encontrado e outras funções que retornam vários objetos que colidiram com o segmento de reta.

Neste artigo, vamos focar em uma das funções de Line Trace. A função é a LineTraceSingleByChannel() que é baseada em channel e retorna o primeiro objeto encontrado. A chamada da função no código de exemplo é feita assim: 

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

Vamos analisar cada parte desta chamada de função:

  • bHitSomething: Variável booleana que armazena o retorno da função. Recebe true se o Line Trace colidiu com um objeto.
  • GetWorld()->LineTraceSingleByChannel(): A função GetWorld() retorna um ponteiro do tipo UWorld que representa o nível atual. A função de Line Trace é chamada a partir deste ponteiro.
  • OutHitResult: Variável do tipo FHitResult. Será preenchida com várias informações relacionadas ao objeto encontrado.
  • StartLocation: FVector que define o início do segmento de reta usado no teste de colisão.
  • EndLocation: FVector que define o fim do segmento de reta.
  • ECC_Visibility: Esta constante indica que será usado o channel Visibility para o teste de colisão.
  • LineTraceParams: Variável do tipo FCollisionQueryParams que pode ser preenchida com valores de parâmetros que serão passados para a função de Line Trace. 


Exemplo de uso:

Para este exemplo será necessário um projeto C++ que tenha o template First Person com o Starter Content. Eu criei um projeto com o nome TutoFirstPerson.

Vamos modificar o Player Character para usar Line Trace para interagir com um abajur de parede. O Player Character precisa estar olhando na direção do abajur que tem de estar a menos de 3 metros. Ao pressionar a tecla E, a luz do abajur irá ligar ou desligar.

Crie uma classe C++ com o nome WallSconce usando como classe pai a classe Actor. Esta classe contém um StaticMeshComponent e um PointLightComponent para representar um abajur. Possui também a função PressSwitch() que será usada pelo jogador para ligar ou desligar a luz do abajur. O arquivo WallSconce.h possui este conteúdo:

#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();
};

A classe WallSconce usa o Static Mesh SM_Lamp_Wall. No construtor temos a configuração dos componentes de Static Mesh e Point Light. A função PressSwitch() chama a função ToggleVisibility() do PointLightComponent. Este é o conteúdo do arquivo WallSconce.cpp:

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

Agora vamos criar um mapeamento de input chamado Interact que será acionado quando o jogador pressionar a tecla E.

No editor de nível, acesse o menu Edit->Project Settings... e na categoria Engine escolha a opção Input. Clique no símbolo + ao lado de Action Mappings, coloque o nome Interact para o novo Action Mapping, e selecione a tecla E. O Action Mappings do projeto vai ficar assim:


Abra o arquivo cabeçalho da classe Character criado pelo template First Person. O nome do arquivo no meu projeto de exemplo é TutoFirstPersonCharacter.h. Adicione a declaração da função InteractWithWorld() abaixo da função SetupPlayerInputComponent():

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

  void InteractWithWorld();

No arquivo cpp do Character é preciso adicionar o #include no início do arquivo:

#include "WallSconce.h"

Na função SetupPlayerInputComponent() será feita a vinculação do Input Interact com a função InteractWithWorld(). Mude o nome ATutoFirstPersonCharacter pelo nome da classe Character do seu projeto e não esqueça de colocar o operador & antes do nome. Ele é usado para retornar o endereço de memória da função. 

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

No final do arquivo cpp, crie a função InteractWithWorld() com o código abaixo. Não esqueça de mudar o nome da classe que está antes do operador "::".

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

Foi feito um Cast<AWallSconce> para verificar se o Actor encontrado pelo Line Trace é da classe AWallSconce. Se for, então será chamada a função PressSwitch() da classe AWallSconce.

Compile o código C++. Coloque uma instância de WallSconce em uma das paredes. Inicie o jogo, vá para perto do WallSconce e olhe em direção a ele. Pressione a tecla E para ligar e desligar o abajur.



Sumário C++