Survival Sample Game

C++ Sample project covering common gameplay concepts packed in a small survival game.

Updated over 2 years ago

Introduction

This series focuses on the C++ aspect of Unreal Engine 4. The goal is to introduce a variety of gameplay concepts written in C++, with some Blueprint interaction to get you more comfortable using C++ for your projects by using practical examples instead of relying on theory.

ItÔÇÖs important to note that this series is not a step-by-step tutorial. Instead each two weeks a new section is published, with all source and assets and associated documentation to explain concepts and the ÔÇ£whyÔÇØ behind some of the code. You can leave questions & feedback on the official forum thread and I will try to integrate user questions back into the source and documentation for future reference.

File:Section6 coopoverview.jpg

Game Premise

The game is a third person survival game focusing on familiar mechanics from games in this genre.

You'll have to find a weapon to defend yourself. Food and ammunition are spread throughout the level and so you are constantly on the search for resources. Enemies may be anywhere, making too much noise while scavenging to survive will attract attention. The environment will have interactive objects to help your defence. The game will support coop play with a buddy. Survive as many days/nights as possible.

You will end up with a basic third person game, fully networked and a small environment with interactive objects that you may use as a base for your own survival game.

Before you get started

ItÔÇÖs not recommended to immediately dive into this sample game if you have no prior C++ experience. There are a few good places to get comfortable with the basics of C++ before moving into more complex concepts such as replication (networking) that are covered in this project.

If you are completely new to Unreal Engine 4 I recommend you first get yourself comfortable with the editor instead. Have a look at the official Getting Started page to get you up to speed or use this collection of links at ZEEF.com for a variety of tutorials and places to learn. And finally, to understand what Actors, PlayerControllers and Pawns represent I recommend reading up on them in the Gameplay Framework section of the docs.

While not required before following this series, I do recommend have a good look at EpicÔÇÖs Coding Standards for C++. I try to keep to this standard throughout the series and itÔÇÖs good to maintain this standard in your own projects to more easily work with the EngineÔÇÖs source and public samples which most of the time hold to this standard.

That is a lot to read up on, but again - I highly recommend doing so before dropping yourself into the survival game project.

If you feel you are ready to get started then go ahead and download the project source at GitHub. There is a release branch available for each Section that is complete, or simply get the latest through the master-branch. You can play the game, look through the code and change/add features, if youÔÇÖre looking for additional information look for the associated Section. If there is no additional info to be found, be sure to grab me on the forums of each individual Section and IÔÇÖll be happy to answer your questions!

Sections Overview

The project is split into 6 sections. Each covering one or more gameplay features or other C++ oriented concepts. You can skip to whatever section you find most interesting as the documentation pages have no dependencies between them.

Project Forum Thread

Section 1 - Character Setup

Be sure to first read the project overview page for information on the project series, recommended documentation and a section overview! The best place for questions and feedback is the official forum thread, it is monitored (the community is often kind enough to help out too!) and will try to answer any question as quickly as possible.

Introduction

This section sets up the third-person character movement with animation, object interaction, simple hunger system, sound and particle playback - all with networking support.

Please consider this documentation a reference guide to get additional information on topics covered in the sample game's source itself. If you are missing a topic or think a specific topic requires more details then feel free to leave your feedback on the section-specific forum thread!

File:Survivalgame section1.jpg

View & download latest project source on GitHub a branch for this section is available too.

Other concepts available in source:

Each section covers a lot more subjects and concepts than I could cover in the docs, if you are interested in learning more you can explore the code and content of the subjects below. Any questions you might have can be answered on the forums of this section.

  • Handling Sound & Particle FX in C++.
  • Overriding existing game framework functionality.
  • Setup of components.
  • Using C++ character movement and behavior data in AnimBlueprint.
  • Advanced MovementComponent to allow sprinting (SCharacterMovementComponent.cpp)
  • Advanced Camera Manipulation with FOV zoom (SCameraManager.cpp)
  • Interacting with objects in the world. (SUsableActor.cpp)

Setting up Input in C++

Binding functions to Key/Mouse input is a fairly simple two-step process. First you bind a string such as "MoveForward" to a function as shown below. The second step is to map this string of "MoveForward" to a key or mouse event by going to  Edit > Project Settings > Input.

  • BindAction() - Provides a trigger to call events like Jump or Throw. You may specify when you wish to run the function in the second parameter.
  • BindAxis() - Useful for Movement and mouse input. The function you bind to this takes a single float as input. For example to move forward with W (1.0) or backward with S (-1.0), these values are manually specified in your project settings Edit > Project Settings > Input.

SCharacter.cpp

// Called to bind functionality to input
void ASCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
    Super::SetupPlayerInputComponent(InputComponent);

    /* Movement */
    InputComponent->BindAxis("MoveForward", this, &ASCharacter::MoveForward);
    InputComponent->BindAxis("MoveRight", this, &ASCharacter::MoveRight);

    /* Looking up/down/sideways is already supported in APawn.h, so we simply reference the existing functions. */
    InputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
    InputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);

    /* There is an overload (meaning a variation with a different set of parameters, but equal funciton name) available to specify when you wish the function to execute. This parameter is only available for BindAction function     and not the above BindAxis. */
    InputComponent->BindAction("Jump", IE_Pressed, this, &ASCharacter::OnStartJump);
    InputComponent->BindAction("Jump", IE_Released, this, &ASCharacter::OnStopJump);
}

SCharacter.h

Example of the different between MoveForward that binds to an Axis, and OnStartJump that binds to an Action.

virtual void MoveForward(float Val);

void OnStartJump();

You can read more on Input here.

Exposing functions and properties to Blueprint.

Unreal Engine 4 supports several C++ Macros that allow you to expose properties and functions to Blueprint. This is very valuable for designers to extend C++ classes with Blueprint behavior and to rapidly tweak or overwrite values for multiple different child Blueprints. The same C++ Macros are used for replication (Networking), serialization (load/save games) etc. Replication is covered in later parts of this section.

SCharacter.h

/* UPROPERTY is a Macro that exposes this variable to UnrealÔÇÖs reflection system, this is how the editor knows how to deal with/visualize this value and to show or hide it as a tweakable value. It has several other purposes too which we will dig into later (such as replication) It is not required on every variable, only when you wish to apply special logic on it like exposing it to Blueprint. */
UPROPERTY(EditDefaultsOnly, Category = "PlayerCondition", Replicated)
float Health;

/* UFUNCTION follows the same theory, but is specialized to functions. In this case we expose the function to Blueprint and place it under ÔÇ£PlayerConditionÔÇØ context menus of the editor. (you can specify any new category you wish. The ÔÇ£constÔÇØ specifier specifies that no value inside this function is changed, it effectively turns this into into readonly. */
UFUNCTION(BlueprintCallable, Category = "PlayerCondition")
float GetMaxHealth() const;

Even if you expose a property to Blueprint itÔÇÖs recommended to assign sensible┬ádefault values in the constructor so a designer can immediately start tweaking when creating a child Blueprint based on the C++ parent.

For more information please refer to Unreal Architecture page.

Performing Ray-traces

Performing ray-traces is one way to finding objects in your scene, if youÔÇÖre new to physics and/or collision in Unreal Engine 4 check out the Collision Responses┬ápage for info on Collision channels, responses and a few common interaction samples.

This section we only use ray-tracing (or line tracing) to retrieve the object the player crosshair is currently looking at. In later sections we will perform line-traces to handle weapon damage and use physics material response to determine damage and particle effect responses.

Performing a line trace is very simple, you need a start, end and length. Another important parameter to be aware of is the collision-channel. In the example below we use ECC_Visibility, any object that does not interact with this channel will let the ray pass through uninterrupted.

Using bTraceComplex ensures we wonÔÇÖt collide with any rough blockers that are used for player motion and that we instead use per-triangle collision checks. Using this is more expensive, but required to avoid frustration of small invisible collision pieces sticking out of nearby objects that interrupt the ray.

/*
    Performs ray-trace to find closest looked-at UsableActor.
*/

ASUsableActor* ASCharacter::GetUsableInView()
{
    FVector CamLoc;
    FRotator CamRot;

    if (Controller == NULL)
        return NULL;

    /* This retrieves are camera point of view to find the start and direction we will trace. */
    Controller->GetPlayerViewPoint(CamLoc, CamRot);
    const FVector TraceStart = CamLoc;
    const FVector Direction = CamRot.Vector();
    const FVector TraceEnd = TraceStart + (Direction * MaxUseDistance);

    FCollisionQueryParams TraceParams(FName(TEXT("TraceUsableActor")), true, this);
    TraceParams.bTraceAsyncScene = true;
    TraceParams.bReturnPhysicalMaterial = false;
    TraceParams.bTraceComplex = true;

    /* FHitResults is passed in with the trace function and holds the result of the trace. */
    FHitResult Hit(ForceInit);
    GetWorld()->LineTraceSingle(Hit, TraceStart, TraceEnd, ECC_Visibility, TraceParams);

    /* Uncomment this to visualize your line during gameplay. */
    //DrawDebugLine(GetWorld(), TraceStart, TraceEnd, FColor::Red, false, 1.0f);

    return Cast<ASUsableActor>(Hit.GetActor());
}

Checking the Type of Actor

Continuing with the code snippet above, we end with a type-cast to our SUsableActor class, if the cast fails it safely returns NULL. And since you should always check your returned pointers, the rest of the calling code will never run (see example below)

ASUsableActor* Usable = GetUsableInView();
if (Usable) /* Is NULL unless we hit an actor and successfully Cast it to SUsableActor */
{
    /* This wonÔÇÖt run unless the cast was succesful, if youÔÇÖre familiar with langu    ages such as C#, if (UsableActor) is equal to if (UsableActor != NULL) in C#. */
    Usable->OnUsed(this);
}

Always make sure you include the correct header for the class you are casting to. In this case we must add...

#include ÔÇ£SUsableActor.hÔÇØ

...to the top of our SCharacter.cpp file. This is true whenever you use your own types in another class, IÔÇÖm simply putting a reminder here - C++ errors may end up looking quite cryptic if you leave this out.

Third person Camera

A third person camera requires some additional setup compared to a first-person viewpoint, but itÔÇÖs still very quick and easy to do. All we need is to attach a spring arm between our character and the camera we wish to place behind our player mesh. The SpringArmComponent supports some neat features┬ásuch as bUsePawnControlRotation to make the attached camera (or other component if we wish) follow the rotation of our character.

ASCharacter::ASCharacter(const class FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<USCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
    // 

    /* The spring component sits between the character and the camera and handles position and rotation of the camera we attach to this spring arm. */
    CameraBoomComp = ObjectInitializer.CreateDefaultSubobject<USpringArmComponent>(this, TEXT("CameraBoom"));

    /* Some defaults to start with, socket is the start and target is position     of our camera. Tweakable in Blueprint. */
    CameraBoomComp->SocketOffset = FVector(0, 35, 0);
    CameraBoomComp->TargetOffset = FVector(0, 0, 55);

    /* Enabling this makes the camera stick on the characterÔÇÖs back. */
    CameraBoomComp->bUsePawnControlRotation = true;
    CameraBoomComp->AttachParent = GetRootComponent();

    /* Simple camera, attached to the spring arm to handle the rotation. */
    CameraComp = ObjectInitializer.CreateDefaultSubobject<UCameraComponent>(this, TEXT("Camera"));

    CameraComp->AttachParent = CameraBoomComp;
}

Extending CameraManager

This is great for some more advanced camera manipulations at runtime such as dynamically changing the Field of View. You can inspect ASPlayerCameraManager.cpp on how that works.

ASPlayerController::ASPlayerController(const class FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
    PlayerCameraManagerClass = ASPlayerCameraManager::StaticClass();
}

And donÔÇÖt forget to include the correct header!

Using Timers

Timers can be very useful in many different gameplay scenarios. In the example we use it for the fuze of the pirate bomb. When the player interacts with the object, the fuze is activated (sound plays and particles start fizzing) and a timer is set as seen below.

void ASBombActor::OnUsed(APawn* InstigatorPawn)
{
    Super::OnUsed(InstigatorPawn);

    if (!bIsFuzeActive)
    {
        // This will trigger the ActivateFuze() on the clients
        bIsFuzeActive = true;

        // Repnotify does not trigger on the server, so call the function here directly.
        SimulateFuzeFX();

        // Active the fuze to explode the bomb after several seconds
        FTimerHandle TimerHandle;
        GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &ASBombActor::OnExplode, MaxFuzeTime, false);
    }
}

After MaxFuzeTime elapses, OnExplode() is called. Since we pass ÔÇ£falseÔÇØ as the final parameter the timer does NOT repeat.

Replication (Networking)

I can recommend everyone interested in making a networked game to read through the Networking & Multiplayer documentation pages.

There is an Networking Intro video on YouTube that explains some additional basics of the server-client pattern.

If you are using replication in your own project make sure you add...

#include "Net/UnrealNetwork.h"

...to your YourProjectName.h file (eg. SurvivalGame.h) in Visual Studio. Otherwise you do not have access to the required macros for replication (such as DOREPLIFETIME(...))

A nice ÔÇ£trickÔÇØ with YourProjectName.h is that any header you include is now available to all class files in the project, but donÔÇÖt go around adding all your custom classes to this file! It will only increase your compilation times.

A general rule in networking is to Never trust the client. All gameplay runs on the server. This means that updating a variable, should be done on the server. Input is triggered on the client, yet we change the value on the server before the other clients will receive this change.

Using Targeting (or Aiming Down Sights) as an example:

/* ÔÇ£TransientÔÇØ is related to serialization, we only care about the ÔÇ£ReplicatedÔÇØ designation for this section. Do remember to always include your variables inside the GetLifetimeReplicatedProps() function or they will not replicate to clients. (as shown below) */
UPROPERTY(Transient, Replicated)
bool bIsTargeting;

A very common convention┬áfrom earlier versions of the engine┬áis to prefix server-side functions with ÔÇ£ServerÔÇØ and explicit client-side functions with ÔÇ£ClientÔÇØ. A function without either one specified in the UFUNCTION macro should not use either prefixes.

Now any client should call SetTargeting(), inside the function we check if we are authoritative (Role == ROLE_Authority) if that check fails, we push the request to the server function which is ServerSetTargeting().

void SetTargeting(bool NewTargeting);

UFUNCTION(Server, Reliable, WithValidation)
void ServerSetTargeting(bool NewTargeting);

There are a few things to know about when dealing with server functions. In your class (cpp) file there is no function that is called ServerSetTargeting(), instead you have ServerSetTargeting_Implementation() and ServerSetTargeting_Validate(). In most cases, your _Implementation should only call the original function, since it is now executed on the server the Role check will pass. and the variables get updated and later replicated back to all clients.

As you can see in the first function, we directly set the variable regardless of Role. This is possible because we used the COND_SkipOwner, replication condition on this variable inside the GetLifetimeReplicatedProps() function (see 2nd example below). With this special replication condition the value is never send back to us, so we have to set it locally anyway.

void ASCharacter::SetTargeting(bool NewTargeting)
{
    bIsTargeting = NewTargeting;

    if (Role < ROLE_Authority)
    {
        ServerSetTargeting(NewTargeting);
    }
}

void ASCharacter::ServerSetTargeting_Implementation(bool NewTargeting)
{
    SetTargeting(NewTargeting);
}

bool ASCharacter::ServerSetTargeting_Validate(bool NewTargeting)
{
    return true;
}

If you browse to Character.cpp you will notice a function like:

void ASCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // Value is already updated locally, so we may skip it in replication step for the owner only (1 client)
    DOREPLIFETIME_CONDITION(ASCharacter, bWantsToRun, COND_SkipOwner);
    DOREPLIFETIME_CONDITION(ASCharacter, bIsTargeting, COND_SkipOwner);
    DOREPLIFETIME_CONDITION(ASCharacter, bIsJumping, COND_SkipOwner);

    // Replicate to every client, no special condition required
    DOREPLIFETIME(ASCharacter, Health);
    DOREPLIFETIME(ASCharacter, Hunger);
}

There are two different replication methods being used here. The first (DOREPLIFETIME_CONDITION(...)) is an optimization, since we already set our value locally, we can skip the owning client of the object. We do want to replicate these values to all other clients (and server) because for example bIsTargeting is used to drive the animation of the characters that are controlled by the other players.

DOREPLIFETIME(...) is the default way, it replicates the value to all clients with no special logic. Now at this stage of the project you might be wondering why we want to replicate this value to other clients (afterall, they do not SEE or NEED this value at this time? And you are correct, we could have used DOREPLIFETIME_CONDITION(..., , COND_OwnerOnly) to reduce network load! But since we are looking ahead, I know I want to include this data into the HUD as healthbars for your party members.

RepNotify

RepNotify is another replication concept so that clients can immediately respond to changes in a variable by the server. Whenever a variable marked with UPROPERTY(..., ┬áReplicatedUsing=OnRep_YourFunction) is updated then ÔÇ£OnRep_YourFunctionÔÇØ gets called, OnRep_ is another standard prefix you should consider using when dealing with RepNotify in your own code.

View SBombActor.cpp for an example on RepNotify. The pirate bomb responds to bIsFuzeActive by activating the sound and particle FX through OnRep_FuzeActive(). A good convention to use when dealing with RepNotify is to split cosmetic features into functions with a "Simulate" prefix. That makes it easier to keep track of the gameplay critical data that must always run on the server, and the cosmetic that is played on clients and non-dedicated servers (eg. a player that acts as the host of the match) only.

Closing

This first section has given us a great springboard for the upcoming sections that will focus more on the gameloop such as enemies, damage dealing and game rules. If you are confused on a particular feature that was covered, feel free to ask about it in the official section 1 thread for this project.

Main Project Page - Next Section (Weapons, Death & Inventory)

File:Section6 advancedanimbp03.jpg

Sets up the third-person character movement with animation, object interaction, simple hunger system, all with networking support.

Section 2 - Weapons, Death & Inventory

Be sure to first read the project overview page for information on the project series, recommended documentation and a section overview! The best place for questions and feedback is the official forum thread, it is monitored (the community is often kind enough to help out too!) and will try to answer any question as quickly as possible.

Introduction

This section adds weapon support for the character, a flashlight, UT-style inventory with on-character visual representation of the carried items and deals with damage, death and respawns for players.

Please consider this documentation a reference guide to get additional information on topics covered in the sample game's source itself. If you are missing a topic or think a specific topic requires more details then feel free to leave your feedback on the section-specific forum thread!

File:Section2 overview02.jpg

File:Flashlight_01..jpg

View & download latest project source on GitHub

Additional concepts introduced in code

  • Arrays (See: SCharacter.h - Inventory)
  • Structs (See: STypes.h - FTakeHitInfo)
  • Enums (See: STypes.h - EInventorySlot)

Concepts

Manipulating materials in C++

File:Section2 materialinstances.jpg

To alter a single object's material during gameplay we have to create a MaterialInstanceDynamic. This gives us a unique instance to change parameters like the flashlight brightness.

First we create the instance. Any new instance must be applied to the mesh, CreateAndSetMaterialInstanceDynamic takes care of both. The first parameter for the material index is usually 0 - your StaticMesh may have more than one material applied using multiple indices (see the Weapons/SK_Rifle mesh for an example)

void ASFlashlight::BeginPlay()
{
    Super::BeginPlay();

    /* Create an instance unique to this actor instance to manipulate emissive intensity */
    USkeletalMeshComponent* MeshComp = GetWeaponMesh();
    if (MeshComp)
    {
        MatDynamic = MeshComp->CreateAndSetMaterialInstanceDynamic(0);
    }
}

If you didn't create this MaterialInstanceDynamic, changing a parameter on a material, would change it on all meshes that use the same material since by default instances of the same materials are shared between meshes.

Now that we have a material to play around with, we can update the brightness of our flashlight like so:

void ASFlashlight::UpdateLight(bool Enabled)
{
    /* Update material parameter */
    if (MatDynamic)
    {       
        /* " Enabled ? MaxEmissiveIntensity : 0.0f " picks between first or second value based on "Enabled" boolean */
        MatDynamic->SetScalarParameterValue(EmissiveParamName, Enabled ? MaxEmissiveIntensity : 0.0f);  
    }
}

The EmissiveParamName contains the FName of the parameter, in this case "Brightness".

The second parameter field may look strange to you, here is a code snippet to explain what it does:

/* This line is a shorthand for the if-statement below */
Enabled ? MaxEmissiveIntensity : 0.0f

float Intensity;
if (Enabled)
{
    Intensity = MaxEmissiveIntensity;
}
else
{
    Intensity = 0.0f;
}

Driving FX with Physical Materials

File:Section2 DrivingFXphysmats.jpg

To spawn particle FX based on the surface we hit, or play a sound based on the surface we standing on we need to know the PhysicalMaterial of a StaticMesh or SkeletalMesh. For StaticMesh this is specified in the shader material in the MaterialEditor, for SkeletalMeshes it's set using the PhatTool on the PhysicsAsset assigned to the SkeletalMesh.

You need to define your own custom Physics Material first. (See: SurvivalGame.h) Default already exists, but for convenience we assign a C++ definition to it. Don't forget to add them to DefaultEngine.ini or the engine won't recognize the types (the C++ code below is only for convenience, the .ini file it where the actual defining is done)

/** when you modify this, please note that this information can be saved with instances
* also DefaultEngine.ini [/Script/Engine.PhysicsSettings] should match with this list **/
#define SURFACE_DEFAULT             SurfaceType_Default
#define SURFACE_FLESH               SurfaceType1

We assign these types to PhysicalMaterial assets. (See: '/Base/PhysMat_Flesh') And the assets are then applied to either (shader) materials or PhysicsAsset (See: '/AnimStarterPack/Character/HeroTPP_Physics')

Now there is one step remaining, our code needs to check the type we hit, and spawn a particle effect, or play a sound based on what we just hit. (See: SImpactEffect.cpp)

void ASImpactEffect::PostInitializeComponents()
{
    Super::PostInitializeComponents();

    /* Figure out what we hit (SurfaceHit is setting during actor instantiation in weapon class) */
    UPhysicalMaterial* HitPhysMat = SurfaceHit.PhysMaterial.Get();
    EPhysicalSurface HitSurfaceType = UPhysicalMaterial::DetermineSurfaceType(HitPhysMat);

    UParticleSystem* ImpactFX = GetImpactFX(HitSurfaceType);
    if (ImpactFX)
    {
        UGameplayStatics::SpawnEmitterAtLocation(this, ImpactFX, GetActorLocation(), GetActorRotation());
    }

    USoundCue* ImpactSound = GetImpactSound(HitSurfaceType);
    if (ImpactSound)
    {
        UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation());
    }
}

The SurfaceHit variable is applied by SWeaponInstant.cpp.

void ASWeaponInstant::SpawnImpactEffects(const FHitResult& Impact)
{
    if (ImpactTemplate && Impact.bBlockingHit)
    {
        /* This function prepares an actor to spawn, but requires another call to finish the actual spawn progress. This allows manipulation of properties before entering into the level */
        ASImpactEffect* EffectActor = GetWorld()->SpawnActorDeferred(ImpactTemplate, Impact.ImpactPoint, Impact.ImpactPoint.Rotation());
        if (EffectActor)
        {
            EffectActor->SurfaceHit = Impact;
            UGameplayStatics::FinishSpawningActor(EffectActor, FTransform(Impact.ImpactNormal.Rotation(), Impact.ImpactPoint));
        }
    }
}

We retrieved this surface information by performing a ray-trace with bReturnPhysicalMaterial = true. (See: SWeapon.cpp)

FHitResult ASWeapon::WeaponTrace(const FVector& TraceFrom, const FVector& TraceTo) const
{
    FCollisionQueryParams TraceParams(TEXT("WeaponTrace"), true, Instigator);
    TraceParams.bTraceAsyncScene = true;
    TraceParams.bReturnPhysicalMaterial = true;

    FHitResult Hit(ForceInit);
    GetWorld()->LineTraceSingle(Hit, TraceFrom, TraceTo, COLLISION_WEAPON, TraceParams);

    return Hit;
}

Attaching/Detaching Actors using Sockets

File:Section2 attachedmeshsockets.jpg

Sockets are great for creating your own attach points on skeletal meshes. To use a socket in C++ you must know the name of the socket as specified through the Skeletal Mesh Editor. See the Docs on how to setup and name Sockets.

In this project we are using it for a couple of things. To attach a weapon to our character's hands, back and pelvis and on the weapon mesh to define the muzzle location. You can use it for many other things too, such as spawning particle FX on your jetpack.

In the code snippet below we retrieve an FName for the slot we wish to attach our weapon to. The AttachTo() function doesn't require you to specify a Bone or Socket, if you omit this value, it will use the root (or pivot) instead.

void ASWeapon::AttachMeshToPawn(EInventorySlot Slot)
{
    if (MyPawn)
    {
        // Remove and hide
        DetachMeshFromPawn();

        USkeletalMeshComponent* PawnMesh = MyPawn->GetMesh();
        FName AttachPoint = MyPawn->GetInventoryAttachPoint(Slot);
        Mesh->SetHiddenInGame(false);
        Mesh->AttachTo(PawnMesh, AttachPoint, EAttachLocation::SnapToTarget);
    }
}

Detaching is even simpler:

void ASWeapon::DetachMeshFromPawn()
{
    Mesh->DetachFromParent();
    Mesh->SetHiddenInGame(true);
}

Dealing & handling damage

File:Section2 dealingdamage.jpg

The engine already have a base framework to support damage. If you're new to Unreal Engine, check out the blog post on Damage in UE4.

An example on dealing damage to an Actor is available in SWeaponInstant.cpp (Note that Actor already includes the function TakeDamage() ) We override and extend this function in our SCharacter.cpp to update Hitpoints and handle death.

void ASWeaponInstant::DealDamage(const FHitResult& Impact, const FVector& ShootDir)
{
    FPointDamageEvent PointDmg;
    PointDmg.DamageTypeClass = DamageType;
    PointDmg.HitInfo = Impact;
    PointDmg.ShotDirection = ShootDir;
    PointDmg.Damage = HitDamage;

    Impact.GetActor()->TakeDamage(PointDmg.Damage, PointDmg, MyPawn->Controller, this);
}

Another way to deal damage is through UGameplayStatics::ApplyRadialDamage (variations exist like ApplyPointDamage and ApplyRadialDamageWithFalloff) The SBombActor uses it to apply damage to anything within the explosion radius.

void ASBombActor::OnExplode()
{
    if (bExploded)
        return;

    // Notify the clients to simulate the explosion
    bExploded = true;
    
    // Run on server side
    SimulateExplosion();

    // Apply damage to player, enemies and environmental objects
    TArray IgnoreActors;
    UGameplayStatics::ApplyRadialDamage(this, ExplosionDamage, GetActorLocation(), ExplosionRadius, DamageType, IgnoreActors, this, nullptr);
}

You can specify your own DamageTypes in content and deal with different types individually (eg. ExplosionDamage, FallDamage etc.) and have each type specify different magnitudes of impulse, if any.

Closing

In this section we've added the basics for our gameloop. In the upcoming sections we will introduce enemies and game objective to put all this to use. If you are confused on a particular feature that was covered, feel free to ask about it in the official section 2 thread for this project.

Previous Section (Character Setup) - Main Project Page - Next Section (Zombie AI)

File:Section6 equipment03.jpg

Adds weapon support for the character, a flashlight, UT-style inventory with on-character visual representation of the carried items and deals with damage, death and respawns for players.

Section 3 - Zombie AI

This section is part of the Survival Game project. You may first want to visit the project main page for a section overview and recommended documentation.

The latest source is available for download through GitHub!

If you have any questions, you can ask them in the official forum thread official forum thread.

Introduction

In section three we introduce the first features for our enemy AI. It can sense a player with both vision (using line of sight checks) and by sensing noise made through footsteps and gun shots. The AI is set up using C++ and a Behavior Tree including a custom Behavior Tree task in C++ to find an appropriate waypoint to wander around the level.

For a step-by-step tutorial on setting up Behavior Trees to follow a sensed player (using Blueprint) see Behavior Tree Quick Start Guide

This section will go into the C++ concepts of dealing with PawnSensing, Blackboards and Behavior Trees in Unreal Engine 4.

File:Survival section3 overview01.jpg

What is a Blackboard?

A blackboard is a data container for the Behavior Tree to use for decision making. A few examples of Blackboard data are TargetLocation, NextWaypoint, TargetPlayer and NeedAmmo.

What is a Behavior Tree?

A Behavior Tree holds the logical tree to drive motion and decisions for a bot. It can be used to search for ammo, follow a player or hide from the player in case hitpoints are low or the bot has no ammo available. It requires a Blackboard to retrieve and store data.

PawnSensing

File:Survival section3 pawnsensing01.jpg

UPawnSensingComponent will give eyes and ears to AI bots. For noise sensing an additional UPawnNoiseEmitterComponent is required on our AI character.

Seeing

PawnSensing supports line of sight checks to sense other pawns. There are a couple of variables to tweak including peripheral-vision angle and sight radius. By default only players are sensed, so we don't need to filter out AI controlled enemies when updating our target to follow.

Hearing

PawnSensing supports hearing of other pawns. This has nothing to do with the actual audio playback in the game, but is a separate system that uses UPawnNoiseEmitterComponent and calls to MakeNoise(...) to trigger events related to noise (eg. footsteps or loud gun noises)

For the AI of this project we implemented footsteps and gun sounds that both call MakeNoise(...). To trigger footstep noise at the appropriate moments of the animation we need a custom AnimNotify as seen below.

The top (yellow) notifies are custom notifies that we bind to our C++ code in the Animation Blueprint to add calls for MakeNoise and to keep track of the last moment a noise was made (to visualize noise in the HUD)

File:Survival section3 animbp animnotify.jpg

File:Survival section3 animbp noiseevents.jpg

The Structural Layout of the Zombie AI

There are many ways of setting up your AI class structure, I will briefly go over the one used in this project to make it easier to dig into the code and follow along with this guide.

SZombieAIController

The AI Controller possesses an AI Character and holds the components for the Blackboard and Behavior Tree. It's the access point to update and retrieve Blackboard data in C++.

SZombieCharacter

Has the components for pawn and noise sensing. Updates Blackboard data through its AI Controller. Contains a Behavior Tree asset that is initialized by the AI Controller on spawn/initialization.

Behavior Tree

Referenced by the AI Character class. Initialized by the AI Controller.

Blackboard

The blackboard is referenced by the Behavior Tree asset.

Setting up the senses in C++

To set up our senses in C++ we need a UPawnSensingComponent in the AI character class.

To react to sense events from this component we bind our delegates (functions that can be hooked to other classes to trigger events, much like you do with binding of mouse and key input to functions in C++) during BeginPlay.

void ASZombieCharacter::BeginPlay()
{
    Super::BeginPlay();

    /* This is the earliest moment we can bind our delegates to the component */
    if (PawnSensingComp)
    {
        PawnSensingComp->OnSeePawn.AddDynamic(this, &ASZombieCharacter::OnSeePlayer);
        PawnSensingComp->OnHearNoise.AddDynamic(this, &ASZombieCharacter::OnHearNoise);
    }
}

When either of these functions are called, they will update the Blackboard with a new move-to target (the sensed player character) through the AI Controller of the AI Character instance.

Do note that to support hearing the AI requires a UPawnNoiseEmitterComponent to receive data from any MakeNoise(...) calls other Pawns may produce. (We add this component in SBaseCharacter.h class)

Setting up the AI Controller

The AI Controller contains the components for Blackboard and Behavior Trees (Although note that the behavior tree itself resides in the AI Character so we may re-use the same AIController class with different bot behaviors) It is the gateway to update data to the Blackboard and runs any available Behavior Tree that was provided by the AI Character it possesses.

Initializing Blackboard & Behavior Tree

Whenever a bot is initialized or respawned it will be possessed by an AI Controller. This is the moment to initialize the Blackboard and run the Behavior Tree to start the bot decision making.

void ASZombieAIController::Possess(class APawn* InPawn)
{
    Super::Possess(InPawn);

    ASZombieCharacter* ZombieBot = Cast(InPawn);
    if (ZombieBot)
    {
        if (ZombieBot->BehaviorTree->BlackboardAsset)
        {
            BlackboardComp->InitializeBlackboard(*ZombieBot->BehaviorTree->BlackboardAsset);

            /* Make sure the Blackboard has the type of bot we possessed */
            BlackboardComp->SetValueAsEnum(BotTypeKeyName, (uint8)ZombieBot->BotType);
        }

        BehaviorComp->StartTree(*ZombieBot->BehaviorTree);
    }
}

Updating Blackboard data

When new sense data is available it must be updated in the Blackboard for the Behavior Tree to use. For this we need the KeyName (eg. "TargetLocation" as specified in the Blackboard asset) and the Blackboard Component. Below is one example of how we can push this data into the Blackboard.

void ASZombieAIController::SetTargetEnemy(APawn* NewTarget)
{
    if (BlackboardComp)
    {
        BlackboardComp->SetValueAsObject(TargetEnemyKeyName, NewTarget);
    }
}

Breaking down the Behavior Tree

File:Survival section3 bt03.jpg

The Behavior Tree steers the decisions and motion of our AI bot. These decisions are based on the available data in the Blackboard asset.

Chasing the player

File:Survival section3 chasingplayer.jpg

Whenever a player is sensed and the TargetEnemy is updated by the bot class in C++ we will successfully pass "Has Sensed Enemy" and move to "Has Target Location" which is set by the same AI Character C++ class. That should succeed and move into "Move to Sensed Player" to finally move to the TargetLocation.

"Has Sensed Player" has "Aborts Lower Priority" set up so we can immediately cancel our any other running behaviors when this value changes. This is used in this particular tree to cancel the patrol/wandering behavior on the right side of the tree.

Wandering the map

File:Survival section3 waypoints01.jpg

By default the bot is set to Passive (this is a custom Enum we created in STypes.h), this completely disables the wander/patrol part of this blackboard through the conditional "Should Wander" check in the tree.

When enabled it will try to locate a Waypoint object in the level through the "Find Bot Waypoint" task. This is a custom task we created in C++ to search for objects on the map of the SBotWaypoint type. When the "Has a Waypoint" succeeds we will continue with another custom task that finds a position on the navigation mesh nearby the Waypoint object we found previously. And finally we "Move to Waypoint".

Both "Find X" tasks in the tree update the blackboard with new data for the other nodes to use (in this case CurrentWaypoint and PatrolLocation are updated by these tasks)

This flow will be cancelled as soon as "Has Sensed Enemy" is successful so sensing an enemy takes priority over wandering around the map.

Notes

  • To work with the AI features of the engine we must include the "AIModule" in SurvivalGame.Build.cs, please don't forget this module if you're re-creating any of these features for your own project.
  • We have several physically simulated barriers in our level, this requires a dynamic Navigation Mesh. To set this up in your own project go to Edit > Project Settings > Navigation Mesh and enable "Rebuild at runtime".
  • When using bots any level must include an encapsulating Nav Mesh Bounds Volume (under Modes > Volumes, see image)

File:Navmeshcreation.jpg

Closing

In this section we've added the basic follow and patrol features for our zombie AI. In upcoming sections we will continue to expand the enemy by attacking an attack ability etc. If you are confused on a particular feature or piece of code, feel free to ask about it in the official section 3 thread for this project.

Previous Section (Weapons, Death & Inventory) - Main Project Page - Next Section (Dynamic Time of Day & Game loop)

File:Section6 zombieattacking01.jpg

Introduces AI "Zombie" enemy to our game using PawnSensing and Behavior Tree.

Section 4 - Dynamic Time of Day & Game loop

C++ Sample project covering common gameplay concepts packed in a small coop survival game.

Introduction

In section 4 of the ongoing survival game series we introduced a dynamic time of day, advanced player spawning and a basic game loop.

If you are not yet familiar with this ongoing project be sure to check out the project overview for information on the series!

File:Survival section4 banner01.jpg

Please consider this documentation a reference guide to get additional information on topics covered in the sample game's source itself. If you have any questions or think a documented concept requires more details then feel free to leave your feedback on the section-specific forum thread!

Time of Day in Multiplayer

File:Survival section4 tod01.jpg

Dynamic time of day is a common feature in (open world) survival games. In this series we use it to drive certain gameplay elements beyond visual aspect such as spawning of enemies and respawning players who died during the night at sunrise. That is entirely GameMode specific and can be omitted if you are only interested in steering your time of day in a networked scenario.

Using the existing features, setting up a dynamic time of day is pretty simple. The primary requirements are, a DirectionalLight set to Movable, an instance of BP_SkySphere (automatically added by the default level template) and finally an Actor that holds the time and update the other actors. For this last one I created STimeOfDayManager.cpp, with BP_TimeOfDayManager to update the sun position of the BP_SkySphere which we cannot access from C++ because it's implemented in Blueprint.

The time of day value is stored in SGameState.cpp. GameState objects exist on both the server and client (unlike GameMode, which is unique to the server) and are a good place to store state data on the world.

Replicated values that should appear smooth (like the sun crawling along the horizon) require some additional smoothing on the client side or the result is a choppy motion as data is only received once every few ticks and can be unpredictable with higher latency or packet loss in actual networking scenarios. The Gamemode class controls the sun position on the server side, updated once every second. The client needs to smooth out this 1-second interval by performing some simple prediction which I will explain below.

Client-side smoothing

The variable that controls the time of day is only updated and replicated once per second in our Gamemode. So we need to apply our own per-frame interpolation to smooth out the sun movement.

We smooth out the movement by keeping track of the last time we received and update for ElapsedGameTime variable (stored in SGameState) and applying a prediction to the sun position based on how long ago that was multiplied by the speed the sun is moving.

// STimeOfDayManager.cpp

void ASTimeOfDayManager::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);

    ASGameState* MyGameState = Cast(GetWorld()->GetGameState());
    if (MyGameState)
    {
        /* Update the position of the sun. */
        if (PrimarySunLight)
        {
            if (LastTimeOfDay == MyGameState->ElapsedGameMinutes)
            {
                TimeSinceLastIncrement += DeltaSeconds;
            }
            else
            {
                /* Reset prediction */
                TimeSinceLastIncrement = 0;
            }

            /* Predict the movement of the sun to smooth out the rotations between replication updates of the actual time of day */
            const float PredictedIncrement = MyGameState->GetTimeOfDayIncrement() * TimeSinceLastIncrement;

            /* TimeOfDay is expressed in minutes, we need to convert this into a pitch rotation */
            const float MinutesInDay = 24 * 60;
            const float PitchOffset = 90; /* The offset to account for time of day 0 should equal midnight */
            const float PitchRotation = 360 * ((MyGameState->ElapsedGameMinutes + PredictedIncrement) / MinutesInDay);

            FRotator NewSunRotation = FRotator(PitchRotation + PitchOffset, 45.0f, 0);
            PrimarySunLight->SetActorRelativeRotation(NewSunRotation);

            LastTimeOfDay = MyGameState->ElapsedGameMinutes;
        }
       }
}

This yields a nice and smooth sun crawling along the sky with a time of day that is only replicated once per second.

Level Setup

The dynamic time of day requires a few Actors to exist and be properly configured in your level. The BP_SkySphere, which is included with default map template. A directional light, that must be set to Movable. A SkyLight Actor, also set to Movable and finally our own Blueprint object BP_TimeOfDayManager (in Content/Base/...)

File:Survival section4 level01.jpg

BP_TimeOfDayManager grabs references to the skylight, directional light and skysphere objects during BeginPlay, so there is no further manual setup required.

Showing HUD Messages using Events

File:Survival section4 hudevents.jpg

When an important gameplay event happens we want to notify the player through the HUD. One example is announcing the sun has fallen and the dangerous night just started. We set up the event function in C++ and make it Blueprint accessible. Now when something interesting happens the Blueprint HUD class can respond by updating a text widget with the text data we sent.

The function is marked BlueprintImplementableEvent so we can setup the HUD in Blueprint to pass the FString data to the appropriate UMG Widget (which our C++ level has no reference to, so we must do this in Blueprint)

// SHUD.cpp

/* An event hook to call HUD text events to display in the HUD. Blueprint HUD class must implement how to deal with this event. */
UFUNCTION(BlueprintImplementableEvent, Category = "HUDEvents")
void MessageReceived(const FString& TextMessage);

The Blueprint implementation simply redirects this event to the UMG widget where we added a function called SetMessage to internally update a Text Block (and perform some timer logic to fade-out after several sections)

File:Survival section4 hudBP01.jpg

This type of event messaging is great for many different interactions between the C++ and your HUD. For example one could use it to display hit notifications when a zombie damaged the player.

Exposing functions to commandline

It's very easy to expose your functions to the command line (~ tilde) input which is incredibly valuable for debugging (eg. infinite ammo, god mode, teleport etc.) All you need is to specify the "exec" keyword with your UFUNCTION() as below.

File:Survival section4 cmdline.jpg

While in-game, type ~ (tilde) and start typing your function name, you will see it appear in the auto completion and it supports parameter passing like the time of day float in the sample above.

// SGameState.cpp

/* By passing in "exec" we expose it as a command line (press ~ to open) */
UFUNCTION(exec)
void SetTimeOfDay(float NewTimeOfDay);

Exposing functions this way is a great tool for debugging, but keep in mind that most commands are only valid on the server.

Spectating other players

File:Survival section4 spectating.jpg

When a player dies and another player is still alive he will spectate the player until he respawns. With the current ruleset respawning happens at sunrise (more on that in the next block "Respawning" below)

In the following code snippet (taken from SPlayerController) you can see we initiate spectating at the UnFreeze() call (the moment the controller receives access to input) which happens almost immediately after death.

The state is updated to Spectating on client and server and by calling ViewAPlayer(direction) we set the spectating focus on the other player that is (possibly) still alive. Finally the HUD is updated to show the spectator screen.

// SPlayerController.cpp

void ASPlayerController::UnFreeze()
{
    Super::UnFreeze();

        // ...

    StartSpectating();
}

void ASPlayerController::StartSpectating()
{
    /* Update the state on server */
    PlayerState->bIsSpectator = true;
    ChangeState(NAME_Spectating);
    /* Push the state update to the client */
    ClientGotoState(NAME_Spectating);

    /* Focus on the remaining alive player */
    ViewAPlayer(1);

    /* Update the HUD to show the spectator screen */
    ClientHUDStateChanged(EHUDState::Spectating);
}

The ViewAPlayer takes care of the camera management of setting it to the 3rd-person view, locked to the remaining player. You can repeatedly call this function to cycle through remaining players while spectating. Keep in mind that we filter out the bots by overwriting the CanSpectate logic of GameMode, see below.

bool ASGameMode::CanSpectate(APlayerController* Viewer, APlayerState* ViewTarget)
{
    /* Don't allow spectating of other non-player bots */
    return (ViewTarget && !ViewTarget->bIsABot);
}

This could be further extended to filter by TeamIndex if multiple teams exist on the map or by checking if the player is still alive.

Respawning

By default the gamemode attempts to spawn players and bots at PlayerStart actors. You can add your own logic on how to pick the best player start, for example by checking if a start is exclusive to a specific team, or if it's a player-only spawn or through a weighting system that does line of sight checks so any spawnpoint out of sight of players is preferred. You simply implement any kind of logic in the ChoosePlayerStart function of GameMode, see "Respawn Using Spawnpoints" below for an example.

For the coop gamemode we respawn players not at PlayerStarts, but near teammates.

Respawn at team using the Navigation Mesh

File:Survival section4 navmeshrespawn.jpg

In the coop ruleset we (re-)spawn at any player that is still alive. For this we utilize the NavigationMesh data that is orignally intended for AI pathfinding. (Press "P" in the map to see the navigation mesh, if your map doesn't have a nav mesh you need to insert a NavMeshBoundsVolume) By using this navigation data we know it will get a valid walking position and we don't accidentally spawn our player into a wall or other blocking Actor.

The following function to respawn the player can be split into 4 distinct parts. First we try to get a position and rotation from the first alive player we can find. If we failed to find a player, we fall back to using PlayerStart Actors instead (as defined in the base class of GameMode) On success however, we query the navigation system to find a random point in a small radius around the other player. The final part is almost identical to the base GameMode function and it's where we create a new pawn, let the controller possess it and finally adjust rotation to match that of the currently alive player.

// SGameMode.cpp

void ASGameMode::RestartPlayer(class AController* NewPlayer)
{
    // ...

    /* Look for a live player to spawn next to */
    FVector SpawnOrigin = FVector::ZeroVector;
    FRotator StartRotation = FRotator::ZeroRotator;
    for (FConstPawnIterator It = GetWorld()->GetPawnIterator(); It; It++)
    {
        ASCharacter* MyCharacter = Cast(*It);
        if (MyCharacter && MyCharacter->IsAlive())
        {
            /* Get the origin of the first player we can find */
            SpawnOrigin = MyCharacter->GetActorLocation();
            StartRotation = MyCharacter->GetActorRotation();
            break;
        }
    }

    /* No player is alive (yet) - spawn using one of the PlayerStarts */
    if (SpawnOrigin == FVector::ZeroVector)
    {
        Super::RestartPlayer(NewPlayer);
        return;
    }

    /* Get a point on the nav mesh near the other player */
    FVector StartLocation = UNavigationSystem::GetRandomPointInRadius(NewPlayer, SpawnOrigin, 250.0f);

    // Try to create a pawn to use of the default class for this player
    if (NewPlayer->GetPawn() == nullptr && GetDefaultPawnClassForController(NewPlayer) != nullptr)
    {
        FActorSpawnParameters SpawnInfo;
        SpawnInfo.Instigator = Instigator;
        APawn* ResultPawn = GetWorld()->SpawnActor(GetDefaultPawnClassForController(NewPlayer), StartLocation, StartRotation, SpawnInfo);
        if (ResultPawn == nullptr)
        {
            UE_LOG(LogGameMode, Warning, TEXT("Couldn't spawn Pawn of type %s at %s"), *GetNameSafe(DefaultPawnClass), &StartLocation);
        }
        NewPlayer->SetPawn(ResultPawn);
    }

    if (NewPlayer->GetPawn() == nullptr)
    {
        NewPlayer->FailedToSpawnPawn();
    }
    else
    {
        NewPlayer->Possess(NewPlayer->GetPawn());

        // If the Pawn is destroyed as part of possession we have to abort
        if (NewPlayer->GetPawn() == nullptr)
        {
            NewPlayer->FailedToSpawnPawn();
        }
        else
        {
            // Set initial control rotation to player start's rotation
            NewPlayer->ClientSetRotation(NewPlayer->GetPawn()->GetActorRotation(), true);

            FRotator NewControllerRot = StartRotation;
            NewControllerRot.Roll = 0.f;
            NewPlayer->SetControlRotation(NewControllerRot);

            SetPlayerDefaults(NewPlayer->GetPawn());
        }
    }
}

Respawn using Spawnpoints

File:Survival section4 spawnpoints.jpg

To spawn enemies and players into our level we setup PlayerStart actors at appropriate points. To designate a specific PlayerStart as "player only" we setup an extension SPlayerStart.cpp with a boolean bPlayerOnly. The base PlayerStart can still be used to spawn the zombie AI and if the bPlayerOnly is TRUE it's automatically the preferred spawn location for any player.

The ChoosePlayerStart function is called by RestartPlayer on respawn. By checking IsSpawnpointAllowed and IsSpawnpointPreferred on all playerstarts we can determine the best spawn position for bots and players.

// SGameMode.cpp

AActor* ASGameMode::ChoosePlayerStart(AController* Player)
{
    TArray PreferredSpawns;
    TArray FallbackSpawns;

    for (int32 i = 0; i < PlayerStarts.Num(); i++)
    {
        APlayerStart* TestStart = PlayerStarts[i];
        if (IsSpawnpointAllowed(TestStart, Player))
        {
            if (IsSpawnpointPreferred(TestStart, Player))
            {
                PreferredSpawns.Add(TestStart);
            }
            else
            {
                FallbackSpawns.Add(TestStart);
            }
        }
    }

    APlayerStart* BestStart = nullptr;
    if (PreferredSpawns.Num() > 0)
    {
        BestStart = PreferredSpawns[FMath::RandHelper(PreferredSpawns.Num())];
    }
    else if (FallbackSpawns.Num() > 0)
    {
        BestStart = FallbackSpawns[FMath::RandHelper(FallbackSpawns.Num())];
    }

    return BestStart ? BestStart : Super::ChoosePlayerStart(Player);
}

/* Check to see if a player and/or AI may spawn at the PlayerStart */
bool ASGameMode::IsSpawnpointAllowed(APlayerStart* SpawnPoint, AController* Controller)
{
    if (Controller == nullptr 

 Controller->PlayerState == nullptr)
        return true;

    /* Check for extended playerstart class */
    ASPlayerStart* MyPlayerStart = Cast(SpawnPoint);
    if (MyPlayerStart)
    {
        return MyPlayerStart->GetIsPlayerOnly() &amp;&amp; !Controller->PlayerState->bIsABot;
    }

    /* Cast failed, Anyone can spawn at the base playerstart class */
    return true;
}

bool ASGameMode::IsSpawnpointPreferred(APlayerStart* SpawnPoint, AController* Controller)
{
    if (SpawnPoint)
    {
        /* Iterate all pawns to check for collision overlaps with the spawn point */
        const FVector SpawnLocation = SpawnPoint->GetActorLocation();
        for (FConstPawnIterator It = GetWorld()->GetPawnIterator(); It; It++)
        {
            ACharacter* OtherPawn = Cast(*It);
            if (OtherPawn)
            {
                const float CombinedHeight = (SpawnPoint->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() + OtherPawn->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()) * 2.0f;
                const float CombinedWidth = SpawnPoint->GetCapsuleComponent()->GetScaledCapsuleRadius() + OtherPawn->GetCapsuleComponent()->GetScaledCapsuleRadius();
                const FVector OtherLocation = OtherPawn->GetActorLocation();

                // Check if player overlaps the playerstart
                if (FMath::Abs(SpawnLocation.Z - OtherLocation.Z) < CombinedHeight &amp;&amp; (SpawnLocation - OtherLocation).Size2D() < CombinedWidth)
                {
                    return false;
                }
            }
        }

        /* Check if spawnpoint is exclusive to players */
        ASPlayerStart* MyPlayerStart = Cast(SpawnPoint);
        if (MyPlayerStart)
        {
            return MyPlayerStart->GetIsPlayerOnly() &amp;&amp; !Controller->PlayerState->bIsABot;
        }
    }

    return false;
}

This is pretty basic implementation of a spawn system. More advanced spawning could include line of sight checks to make sure we don't spawn enemies when a player is looking at that particular spawn point and take distance between spawns and players into account to either spread players out or keep them closely together.

In Closing

In the fourth section we added a basic framework for the gameloop of spawning both players and AI, a replicated time of day, scoring on kills and nights survived as well as a failure state for the game mode.

Section 5 will be announced later this week May 22nd, so keep an eye out on the Announcement Forums!

If you have feedback or questions on this section, feel free to reach out through the official forum thread.

Previous Section (Zombie AI) - Main Project Page - Next Section (Networking)

Notes

Please keep in mind we assigned our SLocalPlayer in DefaultEngine.ini in case you are migrating code into your own project.

[/Script/Engine.Engine]
; Our custom LocalPlayer class
LocalPlayerClassName=/Script/SurvivalGame.SLocalPlayer

Project & Wiki by Tom Looman

File:Section6 timeofday combined.jpg

Introduces a dynamic time of day, advanced player spawning and a basic game loop.

Section 5 - Networking

C++ Sample project covering common gameplay concepts packed in a small coop survival game.

Introduction

In section 5 of the ongoing survival game series we dive into gameplay networking (Replication). If you are not yet familiar with this ongoing project be sure to check out the project overview for information on the series!

Please consider this documentation a reference guide to get additional information on topics covered in the source itself. If you have any questions then feel free to leave your questions & feedback on the section-specific forum thread!

File:Containercity 06.jpg

Networking in Unreal Engine 4

Networking in UE4 is based around the client-server model. This means that there is one server that is authoritative over the game state. All connected clients are kept in close proximity to this state.

Networking in Unreal Engine 4 is referred to as Replication. Therefor a "replicated variable" is one that is kept in sync between client and server where the server maintains and controls the value and the client receives updates to this value to maintain a synchronized state.

The UE4 Docs already have a great deal on Networking & Multiplayer, this page will focus on more practical examples instead covering most commonly used patterns to network a game.

Practical Networking Examples

The best way to learn a difficult topic like replication is through practical examples. The entire sample game is networked and uses a variety of common practices used in many multiplayer games. We'll take one example for each of the common patterns (Server-function, Client-function, NetMulticast and Repnotifies) So let's get started with server functions!

Making the player sprint

Keywords: Server-function (RPC), Reliable/Unreliable, WithValidation, Replication conditions

File:Survival section5 sprinting.jpg

To let the player sprint, we need to update the movement speed on the server, since the server is authoritative of the player's position. The issue we need to solve here is that whenever we press the sprint-key (in our case "LeftShift") on the client's machine we need to push this input event to the server to let it know we want to start sprinting.

In the following code sample we have a function that we mapped to the input key ("LeftShift") input functions are always executed on the client. The input function OnStartSprinting() calls SetSprinting(), which at this time is still executed on the client side. This function performs a Role check to see if we are a client or server and makes the request to the server in case we are calling this on the client.

/* Client-side function mapped to input key */
void ASCharacter::OnStartSprinting()
{
    SetSprinting(true);
}

/* Function that can be called on either server or client. Makes a remote procedure call if called from the client. */
void ASCharacter::SetSprinting(bool NewSprinting)
{
        /* We want to update this variable regardless if a client or server makes the call (therefor it's not placed within a Role == ROLE_Authority check. 
           You generally only want to update (replicated) variables on the server and not client. 
           bWantsToRun is a special case in our SCharacter class since we added a "Replication Condition" which is set up to skip replication of this variable for the owner, all other clients still receive changes to this variable as normal. 
        */
    bWantsToRun = NewSprinting;

    if (Role < ROLE_Authority)
    {
        ServerSetSprinting(NewSprinting);
    }
}

Any "Server" function must be marked with "WithValidation" in the header (in our case inside SCharacter.h) for security reasons. You generally want to include some conservative validation checks. If this check fails the client disconnects from the server. In this case we simply return true without any additional checks.

As of 4.8 you are required to declare the _Implementation and _Validate functions in the header as well (Make sure you class is updated to GENERATED_BODY() instead of the deprecated GENERATED_UCLASS_BODY() at the top of your header or the compiler will warn about duplicate declarations as pre-4.8 code automatically generated these two functions in the header)

/* Server side call to update actual sprint state */
UFUNCTION(Server, Reliable, WithValidation)
void ServerSetSprinting(bool NewSprinting);

void ServerSetSprinting_Implementation(bool NewSprinting);

bool ServerSetSprinting_Validate(bool NewSprinting);

It's recommended to keep the _Implementation functions as simple as possible and simply make a call back to the caller which is SetSprinting().

void ASCharacter::ServerSetSprinting_Implementation(bool NewSprinting)
{
    SetSprinting(NewSprinting);
}

bool ASCharacter::ServerSetSprinting_Validate(bool NewSprinting)
{
    return true;
}

You may have noticed the "Reliable" keyword. This specifies that we require a guarantee this function to be received at the other end. In real networking situations you will get to deal with packet loss. Information may get lost in transit, if this would happen and this function is set to the alternative "Unreliable" we might end up playing the sprinting animation on the client, but the server has never received the request and is still handling the player as if it is simply walking. For one off calls that update the state to new absolute values, using Reliable is recommended, it does add more strain to the networking however, there is definitely a good reason for using Unreliable whenever you can.

An example of what unreliable would be appropriate for is an update to a vector position, if a packet were lost it wouldn't matter so much if we are continuously sending new positions anyway. Using unreliable in those types of situations is highly recommended as there is no reason to re-send packets on failure, or to wait for a packet to arrive before allowing the next packet to be executed as with "Reliable".

Moving back to the first example the variable bWantsToRun is updated both locally on the client and on the server through SetSprinting(...) and ServerSetSprinting which calls SetSprinting as well. This is because we applied a special Replication Condition to this variable to skip the owner of the variable when replicating. This means all other clients still receive updates to this variable, except the owning client, which has already updated the variable locally to begin with.

/* Function that can be called on either server or client. Makes a remote procedure call if called from the client. */
void ASCharacter::SetSprinting(bool NewSprinting)
{
        /* We want to update this variable regardless if a client or server makes the call (therefor it's not placed within a Role == ROLE_Authority check. 
           You generally only want to update (replicated) variables on the server and not client. 
           bWantsToRun is a special case in our SCharacter class since we added a "Replication Condition" which is set up to skip replication of this variable for the owner, all other clients still receive changes to this variable as normal. 
        */
    bWantsToRun = NewSprinting;

    if (Role < ROLE_Authority)
    {
        ServerSetSprinting(NewSprinting);
    }
}
void ASCharacter::GetLifetimeReplicatedProps(TArray&amp; OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // Value is already updated locally, skip in replication step
    DOREPLIFETIME_CONDITION(ASCharacter, bWantsToRun, COND_SkipOwner);
}

A reason for taking this approach to locally updating variables before sending the request is to keep your game responsive when dealing with latency in multiplayer. If your player has to wait for his keypress to change something in-game by routing it through a server it introduces a very noticeable input lag. Players are extremely perceptive to this lag! By immediately updating the variable locally the code to start the sprinting animation can begin immediately and the game will feel more responsive as a result.

Simulating a bomb explosion

Keywords: NetMulticast

File:Survival section5 explosion.jpg

Our bomb actor deals damage to nearby Pawns and shows an explosion particle effect when detonated. The application of damage is done on the server-side only, the particle effect however should spawn on all clients. A great way of handling this is by using NetMulticast which sends a RPC request to all connected clients.

The definition of a NetMulticast functions looks like this:

UFUNCTION(Reliable, NetMulticast)
void SimulateExplosion();

void SimulateExplosion_Implementation();

As with Server and Client functions, only the _Implementation function exists in the Cpp file, calling these functions is still done through the original function name (eg. SimulateExplosion()) instead of the _Implementation variant.

/* This function runs on the server, and applies the gameplay effects like damage
   It also triggers SimulateExplosion() that runs on all clients because of NetMulticast specifier */
void ASBombActor::OnExplode()
{
        // ...

    // Runs on all clients (NetMulticast)
    SimulateExplosion();

        /* Applies damage */
        // ...
}

void ASBombActor::SimulateExplosion_Implementation()
{
    /* This should generally only be used for cosmetic effects or updating collision states etc., all gameplay must run on the server. */

    if (ExplosionSound)
    {
        AudioComp->SetSound(ExplosionSound);
        AudioComp->Play();
    }
    if (ExplosionFX)
    {
        ExplosionPCS->SetTemplate(ExplosionFX);
        ExplosionPCS->ActivateSystem();
    }
}

Now every client will see and hear an explosion effect when the bomb is detonated by a player.

Respawning item pickups

Keywords: Repnotify

File:Survival_section5_cupcakes.jpg

Items that can be picked up in the game still persist and re-appear after a while. When picked up, the object remains in place, but will de-activate any visible effect and mesh until it re-appears through a timer. Whenever this state variable changes, we want all clients to respond by either showing or hiding the mesh of the Actor. For this we can use Repnotify, which is a specifier on a variable that binds a function to be called on any change to that variable.

UPROPERTY(Transient, ReplicatedUsing = OnRep_IsActive)
bool bIsActive;
    
UFUNCTION()
void OnRep_IsActive();

In the implementation of the repnotify function we check the updated value and respond accordingly.

/* Only runs on clients, NOT called on server when a variable changes */
void ASPickupActor::OnRep_IsActive()
{
    if (bIsActive)
    {
        OnRespawned();
    }
    else
    {
        OnPickedUp();
    }
}

void ASPickupActor::GetLifetimeReplicatedProps(TArray &amp; OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ASPickupActor, bIsActive);
}

It's important to keep in mind that any change to a repnotify variable only triggers the function on clients and not the server. So whenever this variable changes, we need to manually call the function if we are the server:

/* This function is called on the server, so we must manually call OnPickedUp when updating bIsActive */
void ASPickupActor::OnUsed(APawn* InstigatorPawn)
{
    // ...
        
        /* notifies all clients, except the server */
    bIsActive = false;

        /* Manually call the function since we are the server and didn't receive the repnotify call */
    OnPickedUp();

        // ...

    Super::OnUsed(InstigatorPawn);
}

Using repnotifies is great for responding to changes in the state of an Actor as we've done in pickup item example above.

Changing the HUD State

Keywords: Client-function (RPC)

File:Survival_section5_spectating.jpg

Depending on the state of the game mode (or player) we show a different HUD. When the player is dead and waiting to respawn we have a spectator HUD, if the game ended we show a Game Over HUD with the total score. These states are updated and evaluated on the server, however the HUD only exists on the Client-side. One way of handling changes to the HUD from a server is to use Client functions. These functions are only executed on the owning client of an object, in this case whomever owns the HUD. Let's dive into an example of a client function.

UFUNCTION(reliable, client)
void ClientHUDStateChanged(EHUDState NewState);

void ClientHUDStateChanged_Implementation(EHUDState NewState);

Its signature is identical to NetMulticast, using an _Implementation function and without _Validate as used for Server-functions. To keep track of the type of function we are dealing with, it's best to use the "Client"-prefix as is done in the engine code.

/* This function is called on the server-side, and requests the client to change the HUD state */
void ASPlayerController::StartSpectating()
{
    /* Perform server-side initializing of spectator code */
        // ...

    /* Update the HUD to show the spectator screen */
    ClientHUDStateChanged(EHUDState::Spectating);
}

/* Only called on the owning client */
void ASPlayerController::ClientHUDStateChanged_Implementation(EHUDState NewState)
{
    ASHUD* MyHUD = Cast(GetHUD());
    if (MyHUD)
    {
        MyHUD->OnStateChanged(NewState);
    }
}

This pattern is very useful for performing client side logic that the client did not request itself in the first place like it would with keyboard input events.

Closing

By now we've covered 4 common replication concepts using a few practical examples from the project. I definitely recommend studying the documentation pages on Networking and Multiplayer, they will help you in understanding the flow of data when dealing with networking in games.

If you have feedback or questions on this section, feel free to reach out through the forums.

Previous Section (Dynamic Time of Day & Game loop) - Main Project Page - Next Section (Polish & Review)

Notes

If you're adding networking to your own game, you must specify the UnrealNetwork header in your ProjectName.h (in our case SurvivalGame.h)

// This is NOT included by default in an empty project! It's required for replication and setting of the GetLifetimeReplicatedProps
#include "Net/UnrealNetwork.h"

Project & Wiki by Tom Looman

File:Section6 coop02.jpg

Introduces game networking and the ability to carry around objects like barriers and bombs.

Section 6 - Polish & Review

C++ Sample project covering common gameplay concepts packed in a small coop survival game.

Introduction

Section 6 is the conclusion to the C++ Survival game series. The final series is upgraded to support the latest 4.8 version of Unreal Engine. It introduces a new coop landscape map along with many tweaks and bug fixes. In this final section I will go over some of the available features in the project and give you some tips on building your own game using this series.

File:Section6 coopoverview.jpg

Feature Highlights

During the course of the 6 sections I covered a variety of C++ gameplay concepts for you to explore and learn from. Not all available features in the project were covered in the section documentation. I invite you to play the game and explore the source to discover everything that was included! To help you get an overview of what is available, I'm highlighting some of the project features below.

Weapons & Equipment

File:Section6 equipment03.jpg

Players spawn with an assault rifle and a flashlight. These weapons can be carried on the character's back (rifle) and pelvis (flashlight) using Sockets. One of the more advanced features included is the custom AnimNotify that calls for the exact moment during the animation we must switch the mesh from being in the character's hands to his back or pelvis. Any time you specify an AnimNotify in your animations you can implement this event in the EventGraph of the AnimationBlueprint for that character.

Advanced Animation Blueprint

File:Section6 advancedanimbp03.jpg

The project includes an advanced animation blueprint setup for the player character. It includes sprinting, crouching, aiming down sights, weapon fire recoil, etc. along with a special "Idle Break" animation that is played whenever a player idle for a small period of time. On top of that AnimMontages are used to blend weapon reloading and equipping.

The animations are from the Animation Starter Pack, freely available on the Unreal Engine marketplace.

Zombie AI

File:Section6 zombieattacking01.jpg

Enemy AI wanders around the area during the night and moves towards noises made by the player. The AI is able to sense gun shots and footsteps and will attack the player on sight in a furious sprint.

The zombie has audio responses for many of the events including when sensing a player, idling, wandering, chasing, getting hit, dying, attacking etc. These audio files were kindly provided by @YorickCoster and can be used in your own projects too!

Dynamic Time of Day

File:Section6 timeofday combined.jpg

The game features a dynamic time of day that is tied to the game mode. As during night time the zombies start spawning into the level and any existing zombies become more active and start wandering around the map. It supports networking with smooth interpolation on clients.

Known issue: The Skylight component does not support efficient runtime re-capture of the environment. To prevent a huge performance loss the lighting is not updated per frame, as a result the day/night isn't completely smooth the moment the sun sets.

Object interaction

File:Section6 bombfuze02.jpg

Some of the objects in the world can be interacted with (Hotkey "E") These include the assault rifle, flashlight, cupcake and the bomb.

Consuming food restores hitpoints and energy while interacting with the bomb sets the fuze. Weapons can be added to the inventory.

Moving Objects

File:Section6 barrier01.jpg

Using the middle-mouse button, any physically simulated object can be picked up and moved around. Examples include the red barrier and the bomb actor. you can rotate the carried objects using the scroll-wheel and the 1 & 2 numeric keys. Left-clicking while carrying an object will throw it in the view direction. This is a neat trick to throw fuzed bombs are your enemy, or to throw food/equipment to your coop buddy.

Available Maps

For this series I've built a few maps to test out specific features. "DefaultMap" is used as the test bed for most newly introduced features.

Coop Landscape

File:Section openworldscenery01.jpg

A recently introduced map for the cooperative gamemode with a slight nudge to an open world.

ContainerCity

File:Section6 containercity01.jpg

Some may recognize it as "Shipment" from a well known shooter. It was used as a testing grounds for the AI sensing and the coop-gamemode.

Coop and Open World

The game was designed as a small 2-player coop survival game where players must survive as many nights as possible while using objects they find in the environment such as bombs, barriers and weapons. Additionally I've included a second gamemode stub you can use to build a more open ruleset for a DayZ/Rust like survival game.

2-Player Coop Survival

File:Section6 coop02.jpg

The gamemode features an accelerated day/night cycle, during the day players can collect items, equipment and setup defenses for the night to come. For the duration of the night zombies appear, any existing zombies become active and start moving around the map. Keep an eye out on your energy levels as low energy will hurt your health. If a player dies during the night, he will respawn at sunset as long as there is at least one other player still alive.

Players score points as they survive the night and by killing zombies. The game continues for as long as there is at least one player alive.

Open World

File:Section6 timeofday03.jpg

The open world gamemode is a stub that can be used to build your own open world ruleset. Unlike the coop mode, this mode is free-for-all (friendlyfire enabled) The mode lacks the scoring system, but supports the time of day and enemy spawns to get you started. Keep in mind that it's a basic setup and will require additional work depending on the design of your own game.

Taking this project beyond

This series is built primarily as a C++ learning resource. I hope you've learned a lot from the previous sections in this series and put that knowledge to use for your own project! If you wish to use this project as a base for your own game, you can!

Upgrade your game world

Don't forget to check out the amazing "A boy and his kite" demo, it's available on the Learn Tab in your Unreal Engine Launcher.

Have a look at the Open World Tools with many great open world rendering improvements introduced in 4.8!

Special Thanks

I'd like to give a special thanks to everyone who helped out during the series. And especially Yorick Coster for providing free to use game audio! The audio files included with the project can be used in your own projects.

  • Yorick Coster (@YorickCoster) for contributing many of the audio files
  • Osman Tsjardiwal (@ozmant) for providing me with ContainerCity base map
  • Everyone on the forums to helped with reporting bugs and feedback
  • The contributors on GitHub

Closing

This section concludes the series, I hope you've learned many new C++ gameplay concepts and use this for your own game projects moving forward!

Follow me on Twitter and let me know what you think of this series! Or check out some of the other things I'm working on right here!

Previous Section (Networking) - Main Project Page

Project & Wiki by Tom Looman

File:Section openworldscenery01.jpg

The final section in the series focuses on bug fixing and a bit of polish to the existing features. This section is compatible with the 4.8 release.

Project & Wiki by Tom Looman