sábado, 16 de janeiro de 2021

Criando Delegates em C++

Neste artigo veremos como criar Delegates em C++. De uma forma simples, podemos definir Delegate como uma referência para uma função.

Unreal Engine possui uma grande variedade de tipos de Delegates que acaba se tornando confuso entender todas essas opções.

Por este motivo, neste artigo vamos focar em um exemplo funcional útil que você pode adaptar ao seu projeto.

Na primeira parte do artigo apresentarei os passos necessários para usar o Delegate, mostrando alguns trechos do código de exemplo. No exemplo de uso você verá o código completo do exemplo.

No exemplo vamos criar uma classe chamada APlatformTrigger. Quando um Actor sobrepor uma instância de APlatformTrigger, será usado um Delegate para avisar que a Plataforma foi ativada. Outras classes podem vincular funções ao Delegate que serão executadas quando a Plataforma for ativada.

Estes são os passos necessários para a criação do Delegate:

Passo 1 - Criar um tipo Delegate

Unreal Engine possui diversas Macros que são usadas para criar um tipo Delegate. O código abaixo criou um tipo Delegate chamado  FDelegateTrigger.

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDelegateTrigger);

Esta Macro deve ficar no arquivo header da classe APlatformTrigger acima da Macro UCLASS().  Este Delegate é DYNAMIC para permitir a vinculação em Blueprints e é MULTICAST para permitir a vinculação de mais de uma função. 

Passo 2 - Definir uma variável usando o tipo Delegate criado

A linha abaixo define uma variável do tipo FDelegateTrigger. Está sendo usado a Macro UPROPERTY(BlueprintAssignable) para permitir a vinculação em Blueprints. 

UPROPERTY(BlueprintAssignable)
FDelegateTrigger OnPlatformTriggered;

Passo 3 - Executar o Delegate

Em nosso exemplo o Delegate será executado quando a Plataforma for sobreposta. Isto é feito chamando a função Broadcast() da variável Delegate.

OnPlatformTriggered.Broadcast();


Os próximos passos são feitos em outra classe C++. Neste exemplo é em uma classe chamada AExplosionHandle.  

Passo 4 - Definir um ponteiro para a classe que tem o Delegate

O ponteiro é definido como UPROPERTY() para que a instância de APlatformTrigger possa ser selecionada no editor de nível. 

UPROPERTY(EditAnywhere, Category = "Delegate Test")
APlatformTrigger* PlatformTriggerInstance;

Passo 5 - Definir uma UFUNCTION() que será vinculada ao Delegate 

No exemplo é uma função chamada Explode() que ativará um sistema de partículas. 

UFUNCTION()
void Explode();

Passo 6 - Vincular a função ao Delegate

A vinculação está sendo feita na função BeginPlay(). É usado o ponteiro PlatformTriggerInstance para chamar a função AddDynamic() da variável Delegate OnPlatformTriggered. 

PlatformTriggerInstance->OnPlatformTriggered.AddDynamic(this,
                                                        &AExplosionHandle::Explode);

Quando o Delegate da classe APlatformTrigger for executado, a função Explode() da classe AExplosionHandle será chamada.

No exemplo de uso veremos também como vincular usando Blueprints.


Exemplo de uso:

Para este exemplo será necessário um projeto C++ que tenha o template Third Person com o Starter Content.

Crie uma classe C++ com o nome PlatformTrigger usando como classe pai a classe Actor. O arquivo PlatformTrigger.h contém a Macro que cria o tipo Delegate FDelegateTrigger e uma variável definida com este tipo:

#pragma once

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

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDelegateTrigger);

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

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

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	
	virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
	
	UPROPERTY(VisibleAnywhere)
	USceneComponent* RootScene;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* StaticMeshComponent;
	
	UPROPERTY(BlueprintAssignable)
	FDelegateTrigger OnPlatformTriggered;
};

A classe PlatformTrigger usa o Static Mesh Shape_Cylinder. A altura foi reduzida modificando a escala para 0.1 no eixo Z. O Delegate é executado na função NotifyActorBeginOverlap(). Este é o conteúdo do arquivo PlatformTrigger.cpp

#include "PlatformTrigger.h"

// Sets default values
APlatformTrigger::APlatformTrigger()
{
  // Set this actor to call Tick() every frame.
  PrimaryActorTick.bCanEverTick = true;
	
  RootScene = CreateDefaultSubobject<USceneComponent>("RootScene");
  RootComponent = RootScene;

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

  if (MeshFile.Succeeded())
  {
    StaticMeshComponent->SetStaticMesh(MeshFile.Object);
  }
  
  StaticMeshComponent->SetCollisionResponseToAllChannels(
                                      ECollisionResponse::ECR_Overlap);
  StaticMeshComponent->SetRelativeScale3D(FVector(1.0f, 1.0f, 0.1f));
}


void APlatformTrigger::NotifyActorBeginOverlap(AActor* OtherActor)
{
  Super::NotifyActorBeginOverlap(OtherActor);
	
  OnPlatformTriggered.Broadcast();
}

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

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

Crie outra classe C++ com o nome ExplosionHandle usando como classe pai a classe Actor. Esta classe tem um ponteiro para a classe PlatformTrigger e tem uma UFUNCTION() que será vinculada ao Delegate. O arquivo ExplosionHandle.h fica assim: 

#pragma once

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

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

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)	
	class UParticleSystemComponent* ParticleSystemComponent;
	
	UPROPERTY(EditAnywhere, Category = "Delegate Test")
	class APlatformTrigger* PlatformTriggerInstance;
	
	UFUNCTION()
	void Explode();
};

No construtor temos a configuração do sistema de partículas que representa a explosão. Na função BeginPlay() é feita a vinculação da função Explode() ao Delegate. Não esqueça de adicionar os #include necessários. Este é o conteúdo do arquivo ExplosionHandle.cpp:

#include "ExplosionHandle.h"
#include "PlatformTrigger.h"
#include "Particles/ParticleSystemComponent.h"

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

  // RootComponent initialization 
  RootScene = CreateDefaultSubobject<USceneComponent>("RootScene");
  RootComponent = RootScene;
  
  // ParticleSystemComponent initialization
  ParticleSystemComponent = CreateDefaultSubobject<UParticleSystemComponent>(
                              TEXT("ParticleSystemComponent"));
  ParticleSystemComponent->SetupAttachment(RootScene);
  ParticleSystemComponent->SetAutoActivate(false);
  
  ConstructorHelpers::FObjectFinder<UParticleSystem> ParticleTemplate(
    TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion"));

  if (ParticleTemplate.Succeeded())
  {
    ParticleSystemComponent->SetTemplate(ParticleTemplate.Object);
  }
  
}

void AExplosionHandle::BeginPlay()
{
  Super::BeginPlay();
	
  if(PlatformTriggerInstance)
  {
    PlatformTriggerInstance->OnPlatformTriggered.AddDynamic(this,
&AExplosionHandle::Explode);
  }
	
}

void AExplosionHandle::Explode()
{
  ParticleSystemComponent->Activate();	
}

void AExplosionHandle::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);
}

Compile o código C++ e adicione uma instância de PlatformTrigger no nível. Adicione uma instância de ExplosionHandle perto da plataforma e na aba Details da instância, na categoria Delegate Test, selecione uma instância de PlatformTrigger.  

 

Inicie o jogo. Quando você sobrepor a plataforma com o Character, a explosão será ativada.


Agora, vamos vincular um Evento customizado ao Delegate usando Blueprints.

Selecione a instância de PlatformTrigger que está no nível e abra o Level Blueprint.


Clique com o botão direito no Event Graph e escolha a opção Create a Reference to PlatformTrigger.


Arraste a partir do pino azul do node e solte no Event Graph para aparecer o menu de contexto. Pesquise por platform e escolha a opção Assign On Platform Triggered. Isto adicionará o node de Bind e um novo Evento customizado. 


Adicione o Evento BeginPlay e conecte no node Bind Event. Adicione a ação Print String e conecte no node do Evento customizado. Coloque uma mensagem no node Print String e compile o Level Blueprint. O Blueprint final fica assim:


Execute o jogo de novo. Quando a plataforma for acionada, o Delegate chamará a função que ativa a explosão e chamará também o Evento customizado definido em Blueprints: 


Sumário C++