How did I deal with…?
Programming Gameplay Using C++ in Unreal Engine
The goal of this project is to write a reusable gameplay system in C++. I'm achieving this by setting the same C++ logic in every game's level, solving local tasks with Blueprints (BP), and avoiding reprogramming.
These are the steps I'm following to bring together all the gameplay parts in this project:
Interaction in real time
First of all, you have a start screen with three options, one of them is taking you into the game and starting Gameplay. Once there, you have a starting point and gates/doors that open/close from in and outside. The user interface (UI) is always giving you messages and hints:
- The minimap is telling you where you are and where to find the pickups
- Also there is a counter, telling you how many pickups you have collected so far
- The Interface is changing the color of your pickup when you get close, and
- Telling you that you are able to pickup.
At the same time, the UI tells you that you have to press “E” to collect, and, when you do so, that you are actually collecting an important piece of information:
If you try to leave the level without all pickups collected, you get messages and hints on the UI, telling you that you're not ready yet:
There are also some invisible barriers:
You can enter the buildings, go up stairs and get on balconies:
When you reach the penultimate pickup, the UI tells you that there is only one item left to unlock the exit gates.
Wen you collect the last item, you get hints and messages from the UI, telling you that you accomplished the mission.
Once at the exit, the gates open and you can go out to the next level, most commonly interviews with counselors from the city, sharing information and prepping for the next mission.
I'm defining gameplay during pre‑production. This schematics are showing the whole gameplay cycle, start to finish. All contents vary, only the logic of gameplay stay constant, working inside each level:
One Page Design
Storyboard
Also, the storyboard is telling me what actions I am handling for Gameplay. At this stage of the development – block out – I am programming:
- Start and End Screens
- Pickups counter
- Pickups logic
- opening and closing gates/doors
- lock/unlock exit gates, and
- Provisional User Interface
Blueprints before C++
I am visual‑programming using Blueprint Widgets first, before doing a translation to C++:
- Start and End Screens
- The whole UI
- Door animations
- Door overlapping, and
- Initial count of total items present in the level.
This image provides only an overview of the different methods I'm including in the Interaction function. For a closer look at the translations, please click on the Interaction case study card:
Once my prototype is functional, I'm in the position to draw a schematics for the C++ logic:
C++ logic schematic
I'm distributing the the specific C++ logic implementation for this gameplay in smaller nodes:
- My Player Character, calling Interface and Delegates to interact with collectibles and gates, SciFiCharacter.
- An interaction Interface, InteractInterface.
- A list of Delegates
- A base Actor for the Pickups, InteractableBase.
- A base Actor for Gates and Doors, WalkThroughBase.
Interface
I am calling my Interface “InteractInterface” and this is what its header looks like (note the IInterface instead of UInterface in the class name):
class SCIFIPROJ_API IInteractInterface
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnInteract(AActor* Caller);
};
And that's all it takes to declare the Interface. I am dedicating a whole article to Interface in C++ for Unreal Engine API in this site. Click on this workflow break down card to take a look:
Player character
I am calling my player character “SciFiCharacter”. This Actor is holding most of the programming, namely:
- All the delegates sending messages to the UI,
- An additional Input Action for handling interaction with objects in the level,
- An additional capsule to trigger specific collision objects from InteractableBase, and
- All the variables needed to interact with pickups and gates/doors.
Here you can see the additional declarations in header, including all variables and Delegates:
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMission);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FCounter);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHint);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMessage);
/** Interact Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
class UInputAction* InteractAction;
UPROPERTY(VisibleAnywhere, Category = "Trigger Capsule")
class UCapsuleComponent* TriggerCapsule;
public:
//************ Delegates
UPROPERTY(BlueprintAssignable)
FMission StartMission;
UPROPERTY(BlueprintAssignable)
FMission Mission_Accomplished;
UPROPERTY(BlueprintAssignable)
FHint Interact_Empty;
UPROPERTY(BlueprintAssignable)
FHint Interact_OneToGo;
UPROPERTY(BlueprintAssignable)
FHint Interact_Finished;
UPROPERTY(BlueprintAssignable)
FCounter Add_Item;
UPROPERTY(BlueprintAssignable)
FMessage Overlapping_Pickup;
UPROPERTY(BlueprintAssignable)
FMessage All_Pickups_Collected;
protected:
/** Called for interacting input */
void Interact(const FInputActionValue& Value);
public:
UFUNCTION()
void OnOverlapBegin(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult);
UFUNCTION()
void OnOverlapEnd(
UPrimitiveComponent* OverlappedCpom,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex);
private:
AActor* OverlappedActor;
UPROPERTY(EditAnywhere, Category = "Pickup", BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
int32 TotalItems;
UPROPERTY(VisibleAnywhere, Category = "Pickup", BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
int32 CollectedItems;
UPROPERTY(VisibleAnywhere, Category = "Pickup", BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
bool CanPickup;
UPROPERTY(VisibleAnywhere, Category = "Pickup", BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
bool MissionAccomplished;
This is Constructor in Source File, with additional capsule and overlapping Events:
TriggerCapsule = CreateDefaultSubobject(TEXT("Trigger Capsule"));
TriggerCapsule->InitCapsuleSize(42.f, 96.0f);
TriggerCapsule->SetCollisionProfileName(TEXT("Trigger"));
TriggerCapsule->SetupAttachment(RootComponent);
TriggerCapsule->OnComponentBeginOverlap.AddDynamic(this, &ASciFiProjCharacter::OnOverlapBegin);
TriggerCapsule->OnComponentEndOverlap.AddDynamic(this, &ASciFiProjCharacter::OnOverlapEnd);
This is BeginPlay with Delegates and variables definitions:
StartMission.Broadcast();
CollectedItems = 0;
CanPickup = false;
MissionAccomplished = false;
Overlap begins, checking out whether pickups are implementing the Interface or not:
void ASciFiProjCharacter::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
IInteractInterface* Interface = Cast(OtherActor);
if (OtherActor && (OtherActor != this) && OtherComp && Interface)
{
OverlappedActor = OtherActor;
CanPickup = true;
Overlapping_Pickup.Broadcast();
}
}
Overlap ends, updating variables:
void ASciFiProjCharacter::OnOverlapEnd(UPrimitiveComponent* OverlappedCpom, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
OverlappedActor = nullptr;
CanPickup = false;
}
Implementing Interface and Delegates Events when pressing the “E” key in source file:
void ASciFiProjCharacter::Interact(const FInputActionValue& Value)
{
if (OverlappedActor && CanPickup)
{
IInteractInterface* Interface = Cast(OverlappedActor);
if (Interface && (MissionAccomplished == false))
{
Interface->Execute_OnInteract(OverlappedActor, this);
CollectedItems++;
Add_Item.Broadcast();
}
if (CollectedItems == (TotalItems - 1))
{
Interact_OneToGo.Broadcast();
}
if (CollectedItems == TotalItems)
{
CanPickup = false;
MissionAccomplished = true;
Mission_Accomplished.Broadcast();
All_Pickups_Collected.Broadcast();
Interact_Finished.Broadcast();
}
}
else
{
if (OverlappedActor == nullptr && CanPickup == false && MissionAccomplished == false)
{
Interact_Empty.Broadcast();
}
if (OverlappedActor == nullptr && CanPickup == false && MissionAccomplished == true)
{
Interact_Finished.Broadcast();
}
}
}
From C++ to Blueprints to UI
This is the Blueprint to update the counter, reading the integer variables from Player Character, counting the total pickups present in the level, and replacing the text in UI:
This is the Macro replacing text in the Messages Panel:
My goal here is to avoid making changes in C++ every time I want to make text changes in the UI. That's why I'm updating them by hand in the Blueprint. Here you can see the Listeners to the Event Dispatchers:
These are the manual text changes, calling the same macro every time an Delegate triggers an Event:
Another Macro doing the same for the Hints Panel:
And the Event Dispatcher Listeners in the Mission panel:
Last but not least, here is the additional input component for the “E” key:
void ASciFiProjCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked(PlayerInputComponent)) {
//Interacting
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Triggered, this, &ASciFiProjCharacter::Interact);
}
}
InteractableBase
I am calling the Actor for my pickups “InteractableBase” because other levels could require different types of meshes or collisions. When the Interface calls an Event, it destroys the pickup and a Delegate sends a message for the UI. Here is the header with Delegate, Interface and UProperties:
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FInteract);
public:
UPROPERTY(BlueprintAssignable)
FInteract Interacting;
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnInteract(AActor* Caller);
virtual void OnInteract_Implementation(AActor* Caller);
private:
UPROPERTY(VisibleAnywhere, Category = "Component", BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
class USphereComponent* CollisionComp;
UPROPERTY(VisibleAnywhere, Category = "Component", BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* InteractableMesh;
And here is the Actor's source file, with Constructor and Interface implementation:
AInteractable_Base::AInteractable_Base()
{
PrimaryActorTick.bCanEverTick = true;
CollisionComp = CreateDefaultSubobject(TEXT("Collision Component"));
CollisionComp->SetupAttachment(RootComponent);
CollisionComp->SetSphereRadius(50.0f);
InteractableMesh = CreateDefaultSubobject(TEXT("Pickup Mesh"));
InteractableMesh->SetupAttachment(CollisionComp);
}
void AInteractable_Base::OnInteract_Implementation(AActor* Caller)
{
if (GEngine)
{
Interacting.Broadcast();
Destroy();
}
}
The only operation on the pickup at this point is destroying the actor via Interface and sending the message through a Delegate. This is the Pickup Blueprint to change color when overlapping:
WalkThroughBase
I'm naming my base Actor for gates and doors “WalkThroughBase” because there might be other levels with other kind of behavior than rotating doors. It consists of a constructor with a mesh and a collider. Here you can see the header containing the UProperties:
private:
UPROPERTY(VisibleAnywhere, Category = "Component",BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
class UBoxComponent* WalkThroughCollision;
UPROPERTY(VisibleAnywhere, Category = "Component", BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
class UStaticMeshComponent* WalkThroughMesh;
And here is the Constructor in Source file:
AWalkThroughBase::AWalkThroughBase()
{
PrimaryActorTick.bCanEverTick = true;
WalkThroughCollision = CreateDefaultSubobject(TEXT("WalkThrough Collision"));
WalkThroughCollision->SetBoxExtent(FVector(100, 75, 150));
WalkThroughMesh = CreateDefaultSubobject(TEXT("WalkThrough Mesh"));
WalkThroughMesh->SetupAttachment(WalkThroughCollision);
}
Once my doors and gates have a mesh and a collider, I can vary their behavior in Blueprints any way I want. Again, enabling stake‑holders with no programming background to make those changes at a higher level, allowing programmers do their thing at a lower level:
There are many other subtle programming details in this project, but these 4 nodes represent the nucleus of the whole functionality, shared by all levels.
With this simple functionalities, we can confront any situation in every imaginable level and even escalate it to a first person shooter mode.
For a closer look at other dimensions of this project, from the point of view of a Technical Artist, click on the following case studies.