Skip to main content

[Unreal Engine][C++] How to create a simple trigger actor

[Unreal Engine][C++] How to create a simple trigger actor

A Simple Trigger Volume in C++

Used Unreal Engine Version: 4.22

This is the first post of a small series of Unreal Engine C++ Tutorials. Keep in mind, that Unreal’s API changes rapidly and often. I still hope, this may be of some use to others.
Coming from Unity, programming in C++ for Unreal is rather painful. I hope to give you some assistance and make life a little bit easier.
Whay, would you say, should we make our own trigger actor? There is ATriggerVolume, right? Yes, there is, but inheriting from it is difficult and rather undocumented. I tried and failed. Yes, we have to give up some of ATriggerVolume's functionality, but we learn a lot and at least we know exactly what it’s doing.
First, we’ll create a new C++ class, inheriting from Actor, called SimpleTriggerVolume.
Let’s add a protected property to hold a reference to our trigger component:

/** Shape of the trigger volume component. */
UPROPERTY(VisibleAnywhere, Category = "Setup")
UShapeComponent* Shape = nullptr;

We want the choice to use other shapes as well, so let’s define a function that does the setup of this shape, but can be overriden by subclasses that want to use other shapes:

/** Creates a custom UShapeComponent to represent
 *  the trigger volume. */
UFUNCTION()
void SetupShapeComponent();

Now let’s implement it:

void ASimpleTriggerVolume::SetupShapeComponent()
{
  // Create the trigger subobject and set it up
  auto BoxTrigger = CreateDefaultSubobject
                    <UBoxComponent>(FName("Trigger Shape"));
  BoxTrigger->SetBoxExtent(TriggerExtent);
  BoxTrigger->SetGenerateOverlapEvents(true);
  Shape = BoxTrigger;
}

To make sure this hapens, we have to call it in the constructor. We also want our trigger component to be very flexible, like giving us the choice of where to place it, relatively to our trigger actor. For this we add a separate root component:

ASimpleTriggerVolume::ASimpleTriggerVolume()
{
  PrimaryActorTick.bCanEverTick = true;

  // Create a separate root component, so the 
  // trigger volume may be placed relatively
  RootComponent = CreateDefaultSubobject
                  <USceneComponent>(FName("Root Component"));

  SetupShapeComponent();
}

To react to overlapping events in the shape component, we have to declare functions that can be added as delegates with Unreal’s AddDynamic macro. It is important that those functions match the signature of the overlap event. Additionally they have to be UFUNCTIONs:

/** Delegate for Shape's overlap begin event. */
 UFUNCTION()
 void OnTriggerOverlapBegin
 (
   UPrimitiveComponent* OverlappedComponent, 
   AActor* OtherActor, 
   UPrimitiveComponent* OtherComp, 
   int32 OtherBodyIndex, 
   bool bFromSweep, const
   FHitResult& SweepResult
 );

 /** Delegate for Shape's overlap end event. */
 UFUNCTION()
 void OnTriggerOverlapEnd
 (
   UPrimitiveComponent* OverlappedComp, 
   AActor* OtherActor, 
   UPrimitiveComponent* OtherComp, 
   int32 OtherBodyIndex
 );

Again we add some more functions to our header file. One function that binds our delegates to the shape’s events and to extra callback functions that can be overriden to make defining callbacks very easy:

/** Adds callbacks to the shape component's 
    begin and end overlap events. */
UFUNCTION()
void BindTriggerCallbacksToShape();

/** Is run, when OnComponentBeginOverlap()
     is called. Override to add functionality. */
UFUNCTION()
virtual void TriggerCallbackOn();

/** Is run, when OnComponentEndOverlap() is
    called. Override to add functionality. */
UFUNCTION()
virtual void TriggerCallbackOff();

Now you either can write your callback code directly into the functions or leave this to more concrete actors, inheriting from our ASimpleTriggerVolume by overriding (I did this to create a Step On Button). To bind our delegate functions to the shape’s events we implement BindTriggerCallbacksToShape and call it in the BeginPlay() function. Since Unreal caches some stuff it might happen, that this binding happens twice, if you move the AddDynamic macro between BeginPlay and the constructor. To prevent this I remove it first before adding it again:

// SimpleTriggerVolume.cpp

void ASimpleTriggerVolume::BeginPlay()           
{ 
  Super::BeginPlay(); 
  BindTriggerCallbacksToShape();
}

void ASimpleTriggerVolume::BindTriggerCallbacksToShape()
{
  if (Shape)
  {
    // Workaround. Prevents cached constructors to add delegates twice.
    Shape->OnComponentBeginOverlap.RemoveDynamic(
           this, &ASimpleTriggerVolume::OnTriggerOverlapBegin);
    Shape->OnComponentEndOverlap.RemoveDynamic(
           this, &ASimpleTriggerVolume::OnTriggerOverlapEnd);

    Shape->OnComponentBeginOverlap.AddDynamic(
           this, &ASimpleTriggerVolume::OnTriggerOverlapBegin);
    Shape->OnComponentEndOverlap.AddDynamic(
           this, &ASimpleTriggerVolume::OnTriggerOverlapEnd);
  }
}

Now let’s implement two events to be fired when an overlap happens. At the beginning of our header file do:

// SimpleTriggerVolume.h

DECLARE_EVENT(ASimpleTriggerVolume, FSimpleTriggerVolumeEvent)
FSimpleTriggerVolumeEvent TriggerOverlapBeginEvent;
FSimpleTriggerVolumeEvent TriggerOverlapEndEvent;

Now implement the delegate functions, fire the events there and add callback functions:

// SimpleTriggerVolume.cpp

void ASimpleTriggerVolume::OnTriggerOverlapBegin
(
  UPrimitiveComponent* OverlappedComp,
  AActor* OtherActor, 
  UPrimitiveComponent* OtherComp, 
  int32 OtherBodyIndex, 
  bool bFromSweep, 
  const FHitResult& SweepResult
){
  // Prevent self collision and check if only collision
  // to specific actor is wanted.
  if (OtherActor && 
     (OtherActor != this) && 
     (OtherActor == ActorThatTriggers || 
      ActorThatTriggers == nullptr))
  {
    TriggerOverlapBeginEvent.Broadcast();
    TriggerCallbackOn();
  }
}

void ASimpleTriggerVolume::OnTriggerOverlapEnd
(
  UPrimitiveComponent* OverlappedComp, 
  AActor* OtherActor, 
  UPrimitiveComponent* OtherComp, 
  int32 OtherBodyIndex
){
  // Prevent self collision and check if only collision
  // to specific actor is wanted.
  if (OtherActor && 
     (OtherActor != this) && 
     (OtherActor == ActorThatTriggers || 
      ActorThatTriggers == nullptr))
  {
    TriggerOverlapEndEvent.Broadcast();
    TriggerCallbackOff();
  }
}

void ASimpleTriggerVolume::TriggerCallbackOn()
{
  UE_LOG(LogTemp, Warning, 
  TEXT("SimpleTriggerVolume::TriggerCallbackOn(). "
     + "To add functionality, override this function."));
}

void ASimpleTriggerVolume::TriggerCallbackOff()
{
  UE_LOG(LogTemp, Warning, 
  TEXT("SimpleTriggerVolume::TriggerCallbackOff(). "
     + "To add functionality, override this function."));
}

If you want to react to overlap events in other actors, you’ll have to get hold of this actor and add callbacks to it’s both event handlers. Define a reference property in the header file, to populate it in the editor:

UPROPERTY(EditAnywhere, Category = "Setup")
ASimpleTriggerVolume* Trigger;

And in the BeginPlay() add you callbacks as lambdas:

if (Trigger)
{ 
  Trigger->TriggerOverlapBeginEvent.AddLambda(
           [this]() { EnablePlatform(); });
  Trigger->TriggerOverlapEndEvent.AddLambda(
           [this]() { EnablePlatform(); });
}

See the complete implementation in the following area. In the next article I’ll show you how to make a step on button with this.

/* SimpleTriggerVolume.h */
#pragma once

#include "Components/ShapeComponent.h"
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SimpleTriggerVolume.generated.h"

/**
 * ASimpleTriggerVolume provides a simple actor that
 * reacts to overlapping actors and fires events on
 * overlap begin and overlap end. The default shape
 * is a box. If other shapes are needed, inherit from
 * it and override SetupShapeComponent().
 *
 * To react to the overlap events, override
 * TriggerCallbackOn() and TriggerCallbackOff().
 */
UCLASS()
class BEATEMDOWN_API ASimpleTriggerVolume : public AActor
{
 GENERATED_BODY()
 
public: 
 DECLARE_EVENT(ASimpleTriggerVolume, FSimpleTriggerVolumeEvent)
 FSimpleTriggerVolumeEvent TriggerOverlapBeginEvent;
 FSimpleTriggerVolumeEvent TriggerOverlapEndEvent;

 // Sets default values for this actor's properties
 ASimpleTriggerVolume();

protected:

 /** Shape of the trigger volume component. */
 UPROPERTY(VisibleAnywhere, Category = "Setup")
 UShapeComponent* Shape = nullptr;

 /** Defines, to which actor this trigger should react to.
     If nullptr, all actors are accepted. */
 UPROPERTY(EditAnywhere, Category = "Setup")
 AActor* ActorThatTriggers = nullptr;

 UPROPERTY(EditAnywhere, Category = "Geometry")
 FVector TriggerExtent = FVector(50.f, 50.f, 50.f);

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

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

protected:

 /** Delegate for Shape's overlap begin event. */
 UFUNCTION()
 void OnTriggerOverlapBegin
 (
  UPrimitiveComponent* OverlappedComponent, 
  AActor* OtherActor, 
  UPrimitiveComponent* OtherComp, 
  int32 OtherBodyIndex, 
  bool bFromSweep, const
  FHitResult& SweepResult
 );

 /** Delegate for Shape's overlap end event. */
 UFUNCTION()
 void OnTriggerOverlapEnd
 (
  UPrimitiveComponent* OverlappedComp, 
  AActor* OtherActor, 
  UPrimitiveComponent* OtherComp, 
  int32 OtherBodyIndex
 );

 /** Creates a custom UShapeComponent to represent
     the trigger volume. */
 UFUNCTION()
 void SetupShapeComponent();

 /** Adds callbacks to the shape component's begin
     and end overlap events. */
 UFUNCTION()
 void BindTriggerCallbacksToShape();

 /** Is run, when OnComponentBeginOverlap() is called.
     Override to add functionality. */
 UFUNCTION()
 virtual void TriggerCallbackOn();

 /** Is run, when OnComponentEndOverlap() is called.
     Override to add functionality. */
 UFUNCTION()
 virtual void TriggerCallbackOff();
};
/* SimpleTriggerVolume.cpp */

#include "SimpleTriggerVolume.h"
#include "Components/BoxComponent.h"

ASimpleTriggerVolume::ASimpleTriggerVolume()
{
 PrimaryActorTick.bCanEverTick = true;

 // Create a separate root component, so the trigger
 // volume may be placed relatively
 RootComponent = CreateDefaultSubobject
                 <USceneComponent>(FName("Root Component"));

 SetupShapeComponent();
}

void ASimpleTriggerVolume::BeginPlay()           
{ 
 Super::BeginPlay(); 
 BindTriggerCallbacksToShape();
}

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

void ASimpleTriggerVolume::SetupShapeComponent()
{
 // Create the trigger subobject and set it up
 auto BoxTrigger = CreateDefaultSubobject
                   <UBoxComponent>(FName("Trigger Shape"));
 BoxTrigger->SetBoxExtent(TriggerExtent);
 BoxTrigger->SetGenerateOverlapEvents(true);
 Shape = BoxTrigger;
}

void ASimpleTriggerVolume::BindTriggerCallbacksToShape()
{
 if (Shape)
 {
  // Workaround. Prevents cached constructors to
  // add delegates twice.
  Shape->OnComponentBeginOverlap.RemoveDynamic(
         this, &ASimpleTriggerVolume::OnTriggerOverlapBegin);
  Shape->OnComponentEndOverlap.RemoveDynamic(
         this, &ASimpleTriggerVolume::OnTriggerOverlapEnd);

  Shape->OnComponentBeginOverlap.AddDynamic(
         this, &ASimpleTriggerVolume::OnTriggerOverlapBegin);
  Shape->OnComponentEndOverlap.AddDynamic(
         this, &ASimpleTriggerVolume::OnTriggerOverlapEnd);
 }
}

void ASimpleTriggerVolume::OnTriggerOverlapBegin
(
  UPrimitiveComponent* OverlappedComp, 
  AActor* OtherActor, 
  UPrimitiveComponent* OtherComp, 
  int32 OtherBodyIndex, 
  bool bFromSweep, 
  const FHitResult& SweepResult
){
 // Prevent self collision and check if only collision
 // to specific actor is wanted.
 if (OtherActor && 
    (OtherActor != this) && 
    (OtherActor == ActorThatTriggers || 
     ActorThatTriggers == nullptr))
    {
  TriggerOverlapBeginEvent.Broadcast();
  TriggerCallbackOn();
    }
}

void ASimpleTriggerVolume::OnTriggerOverlapEnd
(
  UPrimitiveComponent* OverlappedComp, 
  AActor* OtherActor, 
  UPrimitiveComponent* OtherComp, 
  int32 OtherBodyIndex
){
 // Prevent self collision and check if only collision
 // to specific actor is wanted.
 if (OtherActor && 
    (OtherActor != this) && 
    (OtherActor == ActorThatTriggers || 
     ActorThatTriggers == nullptr))
    {
  TriggerOverlapEndEvent.Broadcast();
  TriggerCallbackOff();
    }
}

void ASimpleTriggerVolume::TriggerCallbackOn()
{
 UE_LOG(LogTemp, Warning, TEXT("SimpleTriggerVolume::TriggerCallbackOn(). "
   + "To add functionality, override this function."));
}

void ASimpleTriggerVolume::TriggerCallbackOff()
{
 UE_LOG(LogTemp, Warning, TEXT("SimpleTriggerVolume::TriggerCallbackOff(). "
   + "To add functionality, override this function."));
}

Written with StackEdit.

Unreal Engine C++ Tutorial Series

Want more? Have a look into the following tutorials!

Comments

Popular posts from this blog

Ubuntu 16.04 USB-Stick - "Das Ziel ist schreibgeschützt" lösen

Es gibt Dinge, die dürfen in einem nutzerfreundlichen Betriebssystem einfach nicht passieren. Vor allem dürfen Sie aber nicht monatelang bestehen bleiben. Mit Ubuntu 16.04 kann ich Freunden und Bekannten Ubuntu einfach nicht mehr empfehlen, wenn selbst ich an einfachsten Aufgaben scheitere. Gemeint ist hier das Kopieren von Dateien auf USB-Sticks. Trotz jahrelanger Ubuntu/Linux-Erfahrung gelang es mir erst nach gründlicher Recherche das Problem zu beheben. Ein Laie hat hier keine Chance. Damit ihr nicht lange suchen müsst, hier das Problem samt Lösung: Problem Datei oder Ordner auf Fat32-USB-Stick kopieren oder anlegen schlägt fehl mit der Meldung "Das Ziel ist schreibgeschützt". Lösung das Paket fuse-posixovl installieren und Ubuntu neu starten sudo apt-get install fuse-posixovl Viel Erfolg

Der beste Weg um NintendoDS Schultertasten zu reparieren

Reparatur der NintendoDS Schultertasten ohne Löten! Selbst wenn man seinen NintendoDS wie ein rohes Ei behandelt kommt es doch immer wieder vor, dass nach einiger Zeit eine oder beide der Schultertasten nicht mehr wie gewohnt reagieren. Vor allem der NintendoDS Lite scheint häufig von diesem Problem betroffen zu sein. Wenn man im Internet nach diesem Problem sucht stößt man häufig auf diesen SoftMod: "Lippen über die Taste stülpen und hinein blasen." Diese Lösung funktioniert anfangs relativ gut. Aber der Erfolg ist nur von kurzer Dauer und die Methode von Mal zu Mal weniger erfolgreich. Jetzt bleiben drei Optionen. Bei Nintendo für 59€ ein Austauschgerät holen, eine neue Schultertaste einlöten ODER folgendes (Tipp von meinem Vater): Das wird benötigt: Tri-Wing Schraubenzieher, Elektronisches Reinigungsspray / Kontaktspray Man schraubt den NintendoDS auf (nur machen falls die Garantie abgelaufen ist) und nimmt, nachdem man die Batterie und Batterieklappe entfernt hat, die Rüc