Pages

sábado, 23 de janeiro de 2021

Creating Interfaces in C++

This article is not about user interface (UI). There is a concept in programming called interface, which is a type that has functions that must be implemented by the classes that inherit from the interface. It works as a standard communication protocol between different types of classes.

To better understand, let's create a simple interface that can be used in C++ code.

In the Content Browser, access the folder containing the C++ Classes. Right-click on free space and choose the New C++ Class... option as shown in the image below.


On the next screen, you have to select Unreal Interface as the parent class and click on the Next button.


In the Name field put Interactable. In the Path field, keep the default project folder. Click the Create Class button.


Let's look at the C++ code generated by Unreal Engine for the Interactable interface. I added just one line with the declaration of the Interact() function. This is the contents of the Interactable.h file:

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

Note that two classes have been defined, UInteractable and IInteractable. The UInteractable class inherits from UInterface and uses the UINTERFACE() macro. This class does not need to be modified and exists only to make the interface visible to the Unreal Engine's Reflection system.

The IInteractable class is the one that really represents the interface and will be inherited by other classes. It is in this class that the interface functions are declared.

The virtual keyword used before the Interact() function means that this function can be overridden. When the declaration of a virtual function ends with "= 0", it is a pure virtual function, that is, it has no implementation in the base class.


Example usage:

Let's evolve the example created in the previous article:

Using Line Traces in C++

Let's modify the WallSconce class so that it implements the Interactable interface that was created above. To do this, the WallSconce class must inherit from the IInteractable class.

Delete the PressSwitch() function and add the declaration of the Interact() function. The WallSconce.h file looks like this:

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

In the WallSconce.cpp file, delete the PressSwitch() function and add the definition of the Interact() function:

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

We will use the same input mapping done in the previous article that uses the E key.

We will do two adjustments to the TutoFirstPersonCharacter.cpp file (this name depends on the name of your project). At the beginning of the file, replace the #include from "WallSconce.h" to "Interactable.h":

#include "Interactable.h"

In the InteractWithWorld() function, replace the code inside the if(bHitSomething) block as shown below:

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

A Cast<IInteractable> was made to check if the Actor found by the Line Trace implements the IInteractable interface. If the Cast is successful, the InteractableObject variable receives a valid reference that can be used to call the interface functions.

The TutoFirstPersonCharacter class no longer references the WallSconce class. It only references the Interactable interface. If you create another class that implements the Interactable interface, it is not necessary to modify the code of the TutoFirstPersonCharacter class to interact with the new class.

Compile the C++ code. If you did the example from the previous article, you already have a WallSconce instance on the wall. Start the game and interact with the WallSconce using the E key.


This article concludes part II of the Unreal C++ tutorials.


Table of Contents C++