Survival Game – Character Setup (Section 1)
Reading Time: 8 minutesBe sure to first read the project overview for information on the project series, recommended documentation and a section overview!
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 as a reference guide to get additional information on topics covered in the sample game’s source itself.
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.
- 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)
Setup 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.
// 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); }
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();
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.
/* 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.
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 in this case, we want the precision.
/* 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(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 nullptr (null pointer). 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 languages such as C#, if (UsableActor) is equal to if (UsableActor != NULL) in C#. */ Usable->OnUsed(this); }
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 another component if we wish) follow the rotation of our character.
ASCharacter::ASCharacter(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer.SetDefaultSubobjectClass(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.CreateDefaultSubobjec(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 }(this, TEXT("Camera")); CameraComp->AttachParent = CameraBoomComp;
Extending Camera Manager
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(); }
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)
If you are using replication in your own project make sure you add the following include to any class file that uses replication and needs things like DOREPLIFETIME macros.
#include "Net/UnrealNetwork.h"
A general rule in networking is to never trust the client. All gameplay runs on the server. This means that updating a gameplay critical variable or handling an important function should be done on the server and the client will receive a packet with the value of the new variables and update his state accordingly.
This means that if the client presses a key, the input must be sent to the server to be further handled. In the case of player movement, this will be something like sending the desired direction a client wants to move and the server will handle the collision checks and velocity calculations. To reduce visual latency the client will often predict what the server will do on his own (in this case, move immediately on input) but the server has the final say and will send information back to the client, telling him where he must now be positioned. The final step is that the client will re-adjust his predicted location to the server’s position if the difference is bigger than a slight error margin (depending in your game type)
Using Targeting (or Aiming Down Sights) as an example:
/* We mark this variable for replication, it must be added to GetLifetimeReplicatedProps function in .cpp as well. */ UPROPERTY(Replicated) bool bIsTargeting;
The convention from even before UE4 days is to prefix server-side functions with “Server” and explicit client-side functions with “Client”.
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(). The validate function is for anti-cheat and by default should always return true. The implementation function is where your logic is placed.
Now let’s take a look at the code inside the aim down sights functions:
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; }
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 example below). With this special replication condition, the value is never sent back to us, so we have to set it locally anyway.
If you browse to Character.cpp you will notice a function like:
void ASCharacter::GetLifetimeReplicatedProps(TArray& 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, bIsTargeting, COND_SkipOwner); // Replicate to every client, no special condition required DOREPLIFETIME(ASCharacter, Health); }
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 health bars for your “party members”.
RepNotify
RepNotify is another replication concept so that clients can 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().
Closing
This first section has given us a great springboard for the upcoming sections that will focus more on the game loop such as enemies, damage dealing and game rules. If you are confused about a particular feature that was covered, feel free to comment below!
Good day Tom, I am using your network movement logical in my project and I have a question? when sprinting on a high ping server there seems to be some type of lag where the server will push you forward to catch up, there also some sideways movement jitters thanks.
I’m using all the built-in networking for the character except the sprinting. So long as the movement speed is properly synced on both sides there should be no de-sync. Any desync while those values are same on server/client may be from something else or a bug in Epic’s movement component itself. (You can see in the project how little custom code we have, only the sprint modifier)