sábado, 23 de janeiro de 2021

Criando Interfaces em C++

Este artigo não é sobre interface com usuário (UI). Existe um conceito de programação chamado interface, que é um tipo que possui funções que devem ser implementadas pelas classes que herdam da interface. Funciona como um protocolo padrão de comunicação entre diferentes tipos de classes. 

Para entender melhor, vamos criar uma interface simples que pode ser usada em código C++.

No Content Browser, acesse a pasta que contém as Classes C++. Clique com o botão direito em um espaço livre e escolha a opção New C++ Class... como mostra a imagem abaixo.


Na tela seguinte você tem de selecionar Unreal Interface como classe pai e clicar no botão Next.


No campo Name coloque Interactable. No campo Path, mantenha a pasta padrão do projeto. Clique no botão Create Class.


Vamos analisar o código C++ gerado pela Unreal Engine para a interface Interactable. Eu adicionei apenas uma linha com a declaração da função Interact(). Este é o conteúdo do arquivo Interactable.h:

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Interactable.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UInteractable : public UInterface
{
  GENERATED_BODY()
};


class TUTOFIRSTPERSON_API IInteractable
{
  GENERATED_BODY()

public:

  virtual void Interact(AActor* OtherActor) = 0;
};

Observe que foram definidas duas classes, a UInteractable e a IInteractable. A classe UInteractable herda de UInterface e usa a macro UINTERFACE(). Esta classe não precisa ser modificada e existe apenas para deixar a interface visível para o sistema de Reflexão da Unreal Engine.

A classe IInteractable é a que realmente representa a interface e que será herdada por outras classes. É nesta classe que são declaradas as funções da interface.

A palavra virtual usada antes da função Interact() significa que esta função pode ser sobreposta em uma subclasse. Quando a declaração de uma função virtual termina em "= 0", ela é uma função virtual pura, ou seja, ela não tem implementação na classe base. 


Exemplo de uso:

Vamos evoluir o exemplo criado no artigo anterior:

Usando Line Traces em C++

Vamos modificar a classe WallSconce para que ela implemente a interface Interactable que foi criada acima. Para fazer isso, a classe WallSconce tem de herdar da classe IInteractable.

Delete a função PressSwitch() e acrescente a declaração da função Interact(). O arquivo WallSconce.h fica assim:

#pragma once

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

UCLASS()
class AWallSconce : public AActor, public IInteractable
{
	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;
	
	virtual void Interact(AActor* OtherActor) override;
};

No arquivo WallSconce.cpp, delete a função PressSwitch() e acrescente a definição da função Interact():

#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::Interact(AActor* OtherActor)
{
  PointLightComponent->ToggleVisibility();
  
  FString Message = FString::Printf(TEXT("Switch pressed by %s"), 
                                    *(OtherActor->GetName()));

  if(GEngine)
  {
    GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, Message);
  }
}

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

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

Vamos usar o mesmo mapeamento de input feito no artigo anterior que usa a tecla E.

Faremos apenas dois ajustes no arquivo TutoFirstPersonCharacter.cpp (este nome depende do nome do seu projeto). No início do arquivo, substitua o #include de "WallSconce.h" para "Interactable.h"

#include "Interactable.h"

Na função InteractWithWorld(), substitua o código que está dentro do bloco if(bHitSomething)como mostra abaixo:

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)
  {
    
    IInteractable* InteractableObject = Cast<IInteractable>(OutHitResult.GetActor());
	
    if(InteractableObject)
    {
      InteractableObject->Interact(this);
    }
  }
}

Foi feito um Cast<IInteractable> para verificar se o Actor encontrado pelo Line Trace implementa a interface IInteractable. Se o Cast tiver sucesso, a variável InteractableObject  recebe uma referência válida que pode ser usada para chamar as funções da interface. 

A classe TutoFirstPersonCharacter não referencia mais a classe WallSconce, mas apenas a interface Interactable. Se você criar outra classe que implementa a interface Interactable, não será necessário modificar o código da classe TutoFirstPersonCharacter para interagir com a nova classe.

Compile o código C++. Se você fez o exemplo do artigo anterior, já tem uma instância de WallSconce na parede. Inicie o jogo e interaja com o abajur usando a tecla E.


Este artigo conclui a parte II dos tutoriais de Unreal C++.


Sumário C++