Starting the networked First Person Shooter

Ok, now that we have covered the basics, let's put this information to use and begin work on our network shooter project!

Preparing the NS project

The first thing we need to do is remove a few things that have been provided to us by default in the project. We are going to be removing code from a few of the objects and adding some of our own. I will run through this very quickly, as any significant code changes will be described later in the project. First, navigate to NS.h and change #include "EngineMinimal.h" to #include "Engine.h".

Next, navigate to NSGameMode.h and add the following enum above the ANSGameMode class definition:

UENUM(BlueprintType)
enum class ETeam : uint8
{
    BLUE_TEAM,
    RED_TEAM
};

Here we have a classed enum that will represent the two teams that feature in the NS project. The enum has been classed to be of type uint8. Now that we have this in place, let's work with our ANSCharacter object. We are going to be removing quite a bit of code from this object as we do not need a lot of the generated code. Remove the following code from NSCharacter.h:

    /** Projectile class to spawn */
    UPROPERTY(EditDefaultsOnly, Category=Projectile)
    TSubclassOf<class ANSProjectile> ProjectileClass;

    struct TouchData
    {
        TouchData() { 
            bIsPressed = false;
            Location=FVector::ZeroVector;}
        bool bIsPressed;
        ETouchIndex::Type FingerIndex;
        FVector Location;
        bool bMoved;
    };
    void BeginTouch(const ETouchIndex::Type FingerIndex, const FVector Location);
    void EndTouch(const ETouchIndex::Type FingerIndex, const FVector Location);
    void TouchUpdate(const ETouchIndex::Type FingerIndex, const FVector Location);
    TouchData TouchItem;
    bool EnableTouchscreenMovement(UInputComponent* InputComponent);

Ensure that when you remove any function declarations from the class definition that you remove the corresponding function definition in NSCharacter.cpp, otherwise there will be compiler errors. You must also ensure that you remove the code associated with the ProjectileClass member from ANSCharacter::OnFire() and the if check surrounding the BindAction() method that is used to bind the Fire action mapping to the OnFire() method within the SetupInputComponent() method. Both methods should now appear as follows:

void ANSCharacter::OnFire()
{ 
    // try and play the sound if specified
    if (FireSound != NULL)
        {
UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
    }
    // cont...
void ANSCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
    // set up gameplay key bindings
    check(InputComponent);

        InputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
        InputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
        InputComponent->BindAction("Fire", IE_Pressed, this, &ANSCharacter::OnFire);
        // cont...

If you wish, you may also remove NSProjectile.h and .cpp entirely from the project as we will not be using a standard projectile object for this project. Be sure to remove any references to removed variables as well.

With those changes in place, build the project and ensure there are no compiler errors or warnings. Upon running a PIE session, you will be presented with a simple first person character that can run, jump, and look around with either a controller, or mouse and keyboard.

Making a networked player character

Alright, now we can finally begin adding code to this template project! Let's start by writing the code for our first-person character. Navigate to NSCharacter.h now. In the next section, we will be modifying this class definition to include all of the function declarations and variables we will need for this project.

NSCharacter visual and audio assets

As we are creating an object that is going to be replicated across the network and visually represented on all connected clients, we need to design the visual asset configuration of our ANSCharacter so that it can be viewed properly from two different perspectives. We need to add assets that will appear correctly when viewing the character from a first-person perspective, as will be seen by the owning-client of a character. We must also add assets that will appear correctly when viewing the character from a third-person perspective, as will be seen by all other non-owning clients and server. We also have to figure out a way to have only the first-person assets draw on the owning-client and only the third-person assets draw on the non-owning clients.

Thankfully, this functionality is supported by the USkeletalMeshComponent object. We will cover how to set this up when we start to modify NSCharacter.cpp. For now, add the following to the class definition of ANSCharacter:

/** Pawn mesh: 1st person view (arms; seen only by self) */
UPROPERTY(VisibleDefaultsOnly, Category=Mesh)
class USkeletalMeshComponent* FP_Mesh;

/** Gun mesh: 1st person view (seen only by self) */
UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
class USkeletalMeshComponent* FP_Gun;

/** Gun mesh: 3rd person view (seen only by others) */
UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
class USkeletalMeshComponent* TP_Gun;

Here we have renamed Mesh1P to FP_Mesh. Make sure that whenever you change a variable name, you also update any references to that variable. I would suggest using find and replace. Then we added a new USkeletalMeshComponent handle for a third-person gun mesh (TP_Gun). We have not added a TP_Mesh for the character here as we will use the default mesh handle included in ACharacter. Next we will remove another of the provided members for our own benefit. At about line 41 of the class definition will be a public FVector member called GunOffset. Remove this member and any references to it. Directly under where you removed this member, add/match the following public members:

/** Sound to play each time we fire */
UPROPERTY(EditAnywhere, Category = Gameplay)
class USoundBase* FireSound;

/** Sound to play each time we fire */
UPROPERTY(EditAnywhere, Category = Gameplay)
class USoundBase* PainSound;

/** 3rd person anim montage asset for gun shot */
UPROPERTY(EditAnywhere, Category = Gameplay)
class UAnimMontage* TP_FireAnimaiton;

/** 1st person anim montage asset for gun shot */
UPROPERTY(EditAnywhere, Category = Gameplay)
class UAnimMontage* FP_FireAnimaiton;

/** particle system for 1st person gun shot effect */
UPROPERTY(EditAnywhere, Category = Gameplay)
class UParticleSystemComponent* FP_GunShotParticle;

/** particle system for 3rd person gun shot effect */
UPROPERTY(EditAnywhere, Category = Gameplay)
class UParticleSystemComponent* TP_GunShotParticle;

/** particle system that will represent a bullet */
UPROPERTY(EditAnywhere, Category = Gameplay)
class UParticleSystemComponent* BulletParticle;

UPROPERTY(EditAnywhere, Category = Gameplay)
class UForceFeedbackEffect* HitSuccessFeedback;

Here we have declared the rest of the asset handles we are going to be using. We have two USoundBase handles that will be used to play sounds when a bullet is fired and when the character is hit. Following that, we have the two UAnimMontage asset handles that will be used to play the appropriate gunfire animations for both third and first-person perspectives. Ensure you replace any occurrences of FireAnimation with FP_FireAnimation.

We then have three UParticleSystemComponent handles; the first two will represent our gunshot effects. We have included a first- and third-person version as these two systems will play in different world space positions depending on the perspective through which they are viewed. The last is a particle system we will use to represent our bullet traveling through the world. This is just a particle system this time, as we will not be performing any collision on the bullet itself.

Lastly, we have a UForceFeedbackEffect handle. A force feedback asset is simply a curve type object that allows us to specify with detail how we would like the force feedback in a controller to play. We will use this to inform the client of a successful hit of another player! With all of our handles declared, let's initialize the corresponding components in the constructor. Navigate to NSCharacter.cpp; we are going to be working with the class default constructor ANSCharacter::ANSCharacter(). Here we have already been provided with some code that sets up our base turn rates and initializes the first person camera component, which was provided to us when we generated the project.

We need to change any Mesh1P references to FP_Mesh before we begin adding new code. You will note that when we initialize the FP_Mesh, we call the SetOnlyOwnerSee() method on the object. That will render the FP_Mesh invisible to anything other than the owning character. This means that anything that views this mesh from the third person won't see anything at all. You will also notice that the FP_Gun has been initialized for us. We have set that the owner only sees this mesh as well and we have attached the FP_Gun to the GripPoint socket found with the FP_Mesh. This is exactly the same code we had in the ABMCharacter constructor that we used in BossMode.

Next, we must remove the lines that initialize the GunOffset member as a vector. The last lines of your constructor should now look like this:

    FP_Gun->AttachTo(FP_Mesh, TEXT("GripPoint"), EAttachLocation::SnapToTargetIncludingScale, true);
}

Now that we have made all of the necessary modifications, we can start adding our new code to the constructor by initializing TP_Gun with the following code:

// Create a gun mesh component
TP_Gun = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("TP_Gun"));
TP_Gun->SetOwnerNoSee(true);

TP_Gun->AttachTo(GetMesh(), TEXT("hand_rSocket"), EAttachLocation::SnapToTargetIncludingScale, true);

Here we have called the method SetOwnerNoSee(). This method performs the same functionality as SetOnlyOwnerSee() but the inverse. This will mean that our first person perspective of the player will not see the third-person mesh. This is very important, as the third-person gun mesh will most likely occlude some of the first person camera's view. We also attach this TP_Gun to a socket that will be found in the default skeletal mesh of this object. We have yet to assign this mesh or the socket but we will get around to doing that later when we work with the blueprint abstraction of this object. Underneath this code, add the following:

GetMesh()->SetOwnerNoSee(true);

This will ensure that our third-person character mesh cannot be seen as well. Now we can initialize our particle system components. Add the following code:

// Create particles
TP_GunShotParticle =
CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ParticleSysTP"));

TP_GunShotParticle->bAutoActivate = false;
TP_GunShotParticle->AttachTo(TP_Gun);
TP_GunShotParticle->SetOwnerNoSee(true);

Here we are initializing the third-person gunshot particle system component. We are informing the particle system that we do not wish for it to auto activate, as we only want it to play when a character fires, attaching it to the TP_Gun (as the particle effect needs to play relative to the transform of the TP_Gun mesh). We are also calling SetOwnerNoSee(). Next, add the code for the first person particle:

// Create particle
FP_GunShotParticle= CreateDefaultSubobject<UParticleSystemComponent>(TEXT("FP_GunShotParticle"));

FP_GunShotParticle->bAutoActivate = false;
FP_GunShotParticle->AttachTo(FP_Gun);
FP_GunShotParticle->SetOnlyOwnerSee(true);

Then, finally, add the following for the bullet particle. This particle is to be seen by both third- and first-person perspectives so we needn't worry about who can see it:

BulletParticle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("BulletSysTP"));
BulletParticle->bAutoActivate = false;
BulletParticle->AttachTo(FirstPersonCameraComponent);

That will be all we need for our constructor. Now compile the project and run; we are going to be adding some new classes using the C++ class wizard.

Player states and networking

One of the key concepts of UE4 networking is that a connection is established between a player controller and a server. This connection establishes the net role and ownership of the various network objects that are created during this relationship. However, because of this, player controllers only exist and are replicated on the owning-client (Role_AutonomousProxy) and the server (Role_Authority). The other non-owning versions of a player character do not have a replicated player controller; in fact, if you attempt to access a player controller on a non-owning client version of a character, it will return null. These non-owning clients are in fact controlled by standard AController objects.

This leaves us with an issue: where can we store a grouping of reliably replicated variables that are shared across all instances of a network character? For this, we can use APlayerState objects. Player state objects are members of standard AControllers. A APlayerState object is created for every player on a server (denoted by the number of connected player controllers). These states are replicated to all instances of a connect client and include network-relevant information about the player, such as name, score, health, team, and so on. We are going to be creating an abstraction of the UE4 APlayerState class so that we may add our own variables to that player state, which we can then replicate.

Using the C++ class wizard found under File | New C++ class…, create a new object that inherits from APlayerState and call it NSPlayerState. Once the class has been generated, navigate to NSPlayerState.h and modify the ANSPlayerState class definition to match the following:

UCLASS()
class NS_API ANSPlayerState : public APlayerState
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(Replicated)
    float Health;

    UPROPERTY(Replicated)
    uint8 Deaths;

    UPROPERTY(Replicated)
    ETeam Team;
    
};

Ensure you also add #include "NSGameMode.h" to the include list, otherwise ETeam will be an unknown type. Note that we have changed GENERATED_BODY() to GENERATED_UCLASS_BODY() to take advantage of the FObjectInitializer default constructor. Under this, we have added three replicated variables, Health, Deaths, and Team. We have specified each to be Replicated via the UPROPERTY specifier. As mentioned previously, that will flag these variables for replication. Now navigate to NSPlayerState.cpp. First, we will define the default constructor as follows:

ANSPlayerState::ANSPlayerState(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
    Health = 100.0f;
    Deaths = 0;
    Team = ETeam::BLUE_TEAM;
}

That will simply set the declared member variables to appropriate defaults. Next, we are going to be defining the GetLifetimeReplicatedProps() function that was mentioned previously when we covered replication. Add #include "Net/UnrealNetwork.h" to the #include list for the .cpp and then, under the constructor, add the following function definition:

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

    DOREPLIFETIME(ANSPlayerState, Health);
    DOREPLIFETIME(ANSPlayerState, Deaths);
    DOREPLIFETIME(ANSPlayerState, Team);
}

Just to reiterate, the UBT/UHT will have automatically added a function declaration for this function when it detected the Replicated specifier in our UPROPERTY macros. This may cause intelli-sense bugs as it will not think the function exists. Do not worry, this will compile fine. Within this definition, we have called the super class implementation of the GetLifteTimeReplicatedPropers() method; we then parse each member to the DOREPLIFETIME macro with the ANSPlayerState object type. This will flag these variables to replicate for its lifetime in memory; it will only replicate when the variable is changed to reduce network traffic.

As this is the first piece of networking code we have written, I will be explicit. These are replicated variables that are members of a replicated AActor object (ANSPlayerState). These variables will be replicated to all connected client instances of the corresponding ANSPlayerState. Each connected player will have its own ANSPlayerState that can be accessed in any of its client instances. This means that these variables will have the same value on all of the associated client instances of a particular ANSPlayerState. For example, computer A has a player character with an ANSPlayerState. If computer A's character takes damage, the server instance of computer A's character will reduce the health value stored within its ANSPlayerState to 90. This value will then be replicated to all other client instances of computer A's ANSPlayerState. This means other clients will be able to look at a non-owning client instance and still see the correct player health value (if it was displayed).

Finishing the Player class definition

Compile and run the code to ensure our new ANSPlayerState object compiles. We are now going to finish writing the class definition for the ANSCharacter object. We will start by writing the rest of the member variables we are going to be using for our character calculations. Start by adding the following members of the class definition:

public:
    UPROPERTY(Replicated, BlueprintReadWrite, Category = Team)
    ETeam CurrentTeam;

protected:
        class UMaterialInstanceDynamic* DynamicMat;
        class ANSPlayerState* NSPlayerState;

Here we have a public replicated variable to hold the current team of the player. We have a duplicate of this variable (one here and one in the ANSPlayerState) so we may set the team color of the character before the owning-client instance is possessed by a player controller. This again enforces the difference between player state and character state. Regardless of character, it is still important to save the team in ANSPlayerState in case the associated player gains control of a different game pawn.

Following this, we have a handle for a UMaterialInstanceDynamic; this will be used to set custom base colors on the mesh material so we can use the same material for both the blue and red teams! This will be covered later on in the chapter. We also have a handle to the character's ANSPlayerState that we will obtain when the player character is possessed by a player controller.

Ok, now we can define the rest of our methods for the ANSCharacter. Following the variables we just declared, add the following:

public:
    class ANSPlayerState* GetNSPlayerState();
    void SetNSPlayerState(class ANSPlayerState* newPS);
    void Respawn();

Here we have an accessor method for the ANSPlayerState so that it can be called by external objects. We also declare a setter method for the ANSPlayerState as well. We then declare a respawn method that we will use to inform the game mode of this character's need to respawn. Following this, we will append to the already existing protected methods. In the following code, I have removed the comments that were provided by the FPS C++ template. Ensure that each of these functions is present:

void OnFire();
void MoveForward(float Val);
void MoveRight(float Val);
void TurnAtRate(float Rate);
void LookUpAtRate(float Rate);

// will be called by the server to perform raytrace
void Fire(const FVector pos, const FVector dir);

The only new method here is the Fire() method that will be called by the server when a ray-trace is needed. Under this, we can add our protected methods that override functions that exist in the APawn interface:

// APawn interface
virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;

virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

virtual void BeginPlay();
virtual void PossessedBy(AController* NewController) override;

SetupPlayerInputComponent() will have been provided by the template. The other three methods will be used to drive how the player is damaged, player initialization at begin play, and what happens when an owning-client character is possessed. Under this, in the class definition, you will have the two FORCEINLINE public functions provided by the template. Leave these unchanged. We can now finally add our RPCs for this object!

Writing the ANSCharacter RPCs

Compile and run the code before you continue. Ensure there are no compilation errors; you will receive a few linker errors, however, as we are yet to define some of the functions we overrode from the APawn interface. We are now going to be writing the network remote procedure calls that will drive the networked functionality of our character. For this project, we are going to have a few interactions that take place between the owning client and server. Thankfully, the replication of the player's movement state is handled for us by the replicated APlayerController. We are only responsible for providing the RPCs that will drive any custom functionality.

Add the following to the NSCharacter class definition:

    /** REMOTE PROCEDURE CALLS */
private:
    // Peform fire action on the server
    UFUNCTION(Server, Reliable, WithValidation)
    void ServerFire(const FVector pos, const FVector dir);

    // Multicast so all clients run shoot effects
    UFUNCTION(NetMultiCast, unreliable)
    void MultiCastShootEffects();

    // Called on death for all clients for hilarious death
    UFUNCTION(NetMultiCast, unreliable)
    void MultiCastRagdoll();

    // Play pain on owning client when hit
    UFUNCTION(Client, Reliable)
    void PlayPain();

public:
    // Set's team colour
    UFUNCTION(NetMultiCast, Reliable)
    void SetTeam(ETeam NewTeam);

The previous RPCs are responsible for the following:

  • ServerFire(): This RPC has been specified as a Server RPC, meaning it will be called from the owning client instance and performed on the server. We have also specified that this RPC be called with validation. The purpose of this RPC is to inform that the client has fired its gun and the server instance needs to perform the raytrace to check for a successful hit. We take in a position and direction for the ray trace to this method. We do this as the position and direction of the raytrace will be heavily dependent on the player's mouse position and current camera view information that is only known by the owning-client thus we are parsing this info across the network to the server.
  • MultiCastShootEffect(): This NetMultiCast RPC will be used to inform all connected client instances to play their shoot effects. Remember, NetMultiCasts have to be called on the server, otherwise only the local client instance will invoke the function. This method will be responsible for invoking the third-person effect assets we declared in the class definition earlier. We have specified this as unreliable as this is a non-crucial network call. If it fails, it will have a marginal effect on gameplay.
  • MultiCastRagdoll(): This RPC, also specified as NetMultiCast, will be used to set the 3rd person character mesh to ragdoll on all clients when a player is killed (which will be carried out on the server).
  • PlayPain(): This RPC will be called on the server when it detects that a player has taken damage. As this method is specified as a Client RPC, the function will only execute on the owning client version of the character that has taken damage.
  • SetTeam(): This RPC is the only RPC that is declared as public as it will be invoked by another object (the game mode, but we will get to that later). This has been specified as multicast as we will want all of the connected clients' instances of an ANSCharacter to update its base mesh color to the provided team color.

Defining the ANSCharacter functions

Now that we have declared everything we are going to need for this object, let's provide these declarations definitions. We will start by adding the required GetLifetimeReplicatedProps() definition. Make sure you also add #include "Net/UnrealNetwork.h" to the #include list, as well as #include "NSPlayerState.h", and add the following under the SetupPlayerInputComponent() definition:

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

    DOREPLIFETIME(ANSCharacter, CurrentTeam);
}

Underneath that, we can add the definition for BeginPlay():

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

    if (Role != ROLE_Authority)
    {
        SetTeam(CurrentTeam);
    }
}

Here we have called to the super class implementation of BeginPlay(). Then we have performed a check on the network role of the character. Every AActor has the member Role. This member holds one of the network role states. In this case, we are checking whether this current ANSCharacter is not the authority instance (it is either the owning-client or non-owning client). If so, we want to set the team to whatever value is currently held in CurrentTeam. By now, this variable will have been set to whatever exists on the server instance of this object. We will cover how that works later on in the chapter when we write our game mode. This will ensure that all connected clients share the same material instance.

Dynamic materials and material parameters

Following this, we might as well define our SetTeam() RPC method as this is the only place that it is called in this object. Following the BeginPlay() method, add the following code:

void ANSCharacter::SetTeam_Implementation(ETeam NewTeam)
{
    FLinearColor outColour;

    if (NewTeam == ETeam::BLUE_TEAM)
    {
        outColour = FLinearColor(0.0f, 0.0f, 0.5f);
    }
    else
    {
        outColour = FLinearColor(0.5f, 0.0f, 0.0f);
    }

    if (DynamicMat == nullptr)
    {
        DynamicMat = UMaterialInstanceDynamic::Create(
        GetMesh()->GetMaterial(0), this);

        DynamicMat->SetVectorParameterValue(
        TEXT("BodyColor"), outColour);

        GetMesh()->SetMaterial(0, DynamicMat);
        FP_Mesh->SetMaterial(0, DynamicMat);
    }
}

As stated previously, we must define this function as SetTeam_Implementation as the UBT will automatically replace the function definition with the appropriate versions based on of the UFUNCTION specifiers. Within this method, we are checking what team type has been parsed. We then set a FLinearColor with the appropriate color values, 0.5f in the blue channel for BLUE_TEAM and 0.5f in the red channel for RED_TEAM.

Next, we do something very interesting. Do you remember when we worked with materials in the previous chapters and we created VectorParameter and TextureParameter nodes in our materials? The previous code shows you how to access those parameters by name and set custom values to them via C++.

To do this, however, we must first make a dynamic instance of the base material. Assuming we set the proper material in the blueprint abstraction of this class, we use the UMaterialInstanceDynamic static method Create() to initialize a dynamic instance of whatever base material is applied to our character mesh and save it into the DynamicMat handle we declared in the class definition earlier. We then use this handle to call SetVectorParameterValue(). This method takes in an FName holding the name of the parameter and a vector value to set to it. In this case, we are parsing a linear color. After this, we set the new dynamic material to be that of the 3rd person and first-person meshes. This will ensure that the character meshes (both 1st and 3rd person) will be colored according to their current team.

Getting the player to shoot… online

Ok, now we can define how our player is going to shoot. What we are going to do is add some functionality to the OnFire() method that is currently executed when a player registers input mapped to the Fire action mapping. In our case, this is either the LMB or left trigger on a controller. As this method will only execute on the owning-client, we must invoke any effects we want played on the owning client (1st person) then inform the server of the fire event.

Modify the OnFire() method to match the following:

void ANSCharacter::OnFire()
{ 
    // try and play a firing animation if specified
    if(FP_FireAnimaiton!= NULL)
    {
        // Get the animation object for the arms mesh
        UAnimInstance* AnimInstance = FP_Mesh->GetAnimInstance();
        if(AnimInstance != NULL)
        {
            AnimInstance->Montage_Play(FP_FireAnimaiton, 1.f);
        }
    }
    
    // Play the FP particle effect if specified
    if (FP_GunShotParticle != nullptr)
    {
        FP_GunShotParticle->Activate(true);
    }

As you can see, we have removed the play sound at the location call from the provided definition as we need that sound to play on all clients when a fire event is registered, not just the owning-client. We then inform the FP_GunShotParticle to play. Ok, now add the following code to the definition:

FVector mousePos;
FVector mouseDir;

APlayerController* pController = Cast<APlayerController>(GetController());

FVector2D ScreenPos = GEngine->GameViewport->Viewport->GetSizeXY();

pController->DeprojectScreenPositionToWorld(ScreenPos.X / 2.0f,
ScreenPos.Y / 2.0f,
mousePos,
mouseDir);
mouseDir *= 10000000.0f;

ServerFire(mousePos, mouseDir);

} // <-- closes ANSCharacter::OnFire()

This code will look very similar to the code we used to perform the tracking ray trace in BossMode! We are de-projecting the mouse position from screen space to world so we have a position and a direction for our fire ray trace. We scale the direction by a very large amount as there is no range limit on our gunshot. If you wanted to drive weapon range, it would be done here, by changing this amount we scale the mouse direction. Finally, we parse this information to the server via the ServerFire() RPC we declared earlier. Again, we might as well define this RPC now. Underneath this code, add the following:

bool ANSCharacter::ServerFire_Validate(const FVector pos,
    const FVector dir)
{
    if (pos != FVector(ForceInit) && dir != FVector(ForceInit))
    {
        return true;
    }
    else
    {
        return false;
    }
}

void ANSCharacter::ServerFire_Implementation(const FVector pos, const FVector dir)
{
    Fire(pos, dir);
    MultiCastShootEffects();
}

Here we have included the definition for the validation for this RPC. We have included this to ensure that we are not wasting bandwidth on calls that would be incorrect. It is important to note that it is with these validate functions that you would probably implement anti-cheating measures. You can do this by doing various checks on the input to the method. If anything looks off, you can return false and the RPC will not be invoked. Here we are simply checking that if the vectors provided equal the default initialized vector (0, 0, 0), they must be incorrect, therefore we reject the RPC.

Within the RPC itself, we call the Fire() method we declared earlier that will do the actual ray trace. As we specified that this method is to be a Server RPC, we can ensure that this Fire() method will only be invoked on the server. Following this, we then call the MultiCastShootEffect() MultiCast RPC. The MultiCast RPC will be used to invoke all of the effects that should be seen on all clients (3rd person effects).

Let's define the MultiCastShootEffect() and Fire() methods now. Starting with the MultiCast RPC, add the following code to the .cpp:

void ANSCharacter::MultiCastShootEffects_Implementation()
{
    // try and play a firing animation if specified
    if (TP_FireAnimaiton != NULL)
    {
        // Get the animation object for the arms mesh
        UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
        if (AnimInstance != NULL)
        {
            AnimInstance->Montage_Play(TP_FireAnimaiton, 1.f);
        }
    }

    // try and play the sound if specified
    if (FireSound != NULL)
    {
        UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
    }

    if (TP_GunShotParticle != nullptr)
    {
        TP_GunShotParticle->Activate(true);
    }

    if (BulletParticle != nullptr)
    {
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), BulletParticle->Template, BulletParticle->
        GetComponentLocation(), BulletParticle->
        GetComponentRotation());
    }
}

This method is going to be executed on all connected clients so we need to ensure that we only activate the effects that can be seen by everyone. In the previous method, we are informing the anim instance associated with the default character mesh object to play the TP_FireAnimation. This will result in all of the non-owning client instances of the character to animate some recoil from the gunshot. Following this, we then play the fire sound at the location of the client instance so that attenuation and 3D sound modulation plays as intended. We then activate the TP_GunShotParticle so that other clients will see a small blast effect at the barrel of the TP_Gun mesh.

Finally, we are spawning a new emitter at the location of the BulletParticle component found within the character. We are spawning a new emitter instead of activating the BulletParticle component so we can have multiple instances of the particle system active at any one time. Activate would cause the particle to reset every time the character fires. We do not want this, as we want each bullet particle effect to play for its intended lifetime. Now it is time to define the Fire() method; add the following code below MultiCastShootEffects():

void ANSCharacter::Fire(const FVector pos, const FVector dir)
{
    // Perform Raycast
    FCollisionObjectQueryParams ObjQuery;
    ObjQuery.AddObjectTypesToQuery(ECC_GameTraceChannel1);

    FCollisionQueryParams ColQuery;
    ColQuery.AddIgnoredActor(this);

    FHitResult HitRes;
    GetWorld()->LineTraceSingleByObjectType(HitRes, pos, dir, ObjQuery, ColQuery);

    DrawDebugLine(GetWorld(), pos, dir, FColor::Red, true, 100, 0, 5.0f);

    if (HitRes.bBlockingHit)
    {
            ANSCharacter* OtherChar = Cast<ANSCharacter>(HitRes. GetActor());

        if (OtherChar != nullptr &&
            OtherChar->GetNSPlayerState()->Team !=
            this->GetNSPlayerState()->Team)
        {
            FDamageEvent thisEvent(UDamageType::StaticClass());
            OtherChar->TakeDamage(10.0f, thisEvent, this->
            GetController(), this);

            APlayerController* thisPC = Cast<APlayerController>(GetCo ntroller());

            thisPC->ClientPlayForceFeedback(HitSuccessFeedback, false, NAME_None);
        }
    }
}

Again, this code will look very familiar to that of the code we used in BossMode for our tracking ray cast. In this instance, we are looking for objects that are part of ECC_GameChannel1. This is the custom game channel that we will be using to create the character collision channel. This is specified by the FCollisionObjectQueryParams struct. We then specify that the line trace should ignore the actor this function is being called from via the this keyword. We then perform the ray trace by calling LineTraceSingleByObjecType(). This method will take in our collision query parameter structs. This will return a positive hit on the first object that is collided with that meets the query parameters.

We then check the bBlockingHit member of the FHitResult return type. If a blocking hit is found, we then do a few things. First, we cast the HitActor member of the FHitResult struct to an ANSCharacter. If this cast is successful, we then check that the team of the hit actor and the team of the current actor differ. If this resolves as true, we inform the hit actor to take 10.0f health damage. As we have overridden the TakeDamage function in our ANSCharacter class definition, we will be able to define how this damage is interpreted. Finally, we inform the owning-client of the shooting character of a successful hit via the ClientPlayForceFeedback() method of APlayerController. This will cause the character's owning-client to play the force feedback asset we are going to store in the UForceFeedbackAsset handle declared earlier.

Taking damage and UE4 timers

Now, let's define how our character is going to take damage. Add the following function definition to the .cpp:

float ANSCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);

    if (Role == ROLE_Authority && 
        DamageCauser != this && 
        NSPlayerState->Health > 0)
    {
        NSPlayerState->Health -= Damage;
        PlayPain();


        if (NSPlayerState->Health <= 0)
        {
            NSPlayerState->Deaths++;

            // Player has died time to respawn
            MultiCastRagdoll();
            
           ANSCharacter * OtherChar = 
           Cast< ANSCharacter >(DamageCauser);

            if (OtherChar)
            {
                OtherChar->NSPlayerState->Score += 1.0f;
            }

            // After 3 seconds respawn
            FTimerHandle thisTimer;
								GetWorldTimerManager().SetTimer<ANSCharacter>
           (thisTimer, 
           this, & ANSCharacter::Respawn, 3.0f, false);
           
        }
    }

    return Damage;
}

Here we are calling into the super class implementation of TakeDamage(). We then check that this character meets some requirements before taking damage. This method must be executed on the ROLE_Authority, the damage causer must not be itself, and the current health of the character must be above 0. If all of these conditions are met, we then subtract the damage amount from the health value stored in the ANSPlayerState. As this is a replicated value and it is being set on the server (ROLE_Authority), all other instances of this ANSPlayerState will have the health value update appropriately. We then inform the client to play a pain sound effect via the client PlayPain() RPC.

Following this, we check whether the player's health has fallen below zero. If so, we need to handle player death. The first thing we do is increment the number of deaths on the damaged actor. We then call the MultiCastRagdoll() method so that all connected clients witness the death via a ragdoll animation. We then check that the other actor is of type ANSCharacter via casting. If the cast is successful, we increment the score of the other player as they just killed an enemy.

Now we need to be able to respawn the character that was just set to ragdoll in 3 seconds. We do this using the FTimerManager, much like we did in Bounty Dash when we were working the object and coin spawners. We create a timer that will execute the respawn function after 3 seconds have passed. This will ensure we have plenty of time to witness the enemy character's mesh ragdoll after death before being respawned. Next, we will be defining the various functions that are referenced in the TakeDamage() function definition. First, we will define the client PlayPain RPC:

void ANSCharacter::PlayPain_Implementation()
{
    if (Role == ROLE_AutonomousProxy)
        {
            UGameplayStatics::PlaySoundAtLocation(this, PainSound, GetActorLocation());
    }
}

This simply plays the pain sound we specify via the handle at the owning-client's actor location. We can ensure that this sound is only played on the owning client via the Role == ROLE_AutonomousProxy) if statement. Under this, add the following method definition:

void ANSCharacter::MultiCastRagdoll_Implementation()
{
    GetMesh()->SetPhysicsBlendWeight(1.0f);
    GetMesh()->SetSimulatePhysics(true);
    GetMesh()->SetCollisionProfileName("Ragdoll");
}

This is the multicast RPC that will set the dead character's mesh to ragdoll on all connected clients. It simply uses the GetMesh() method to get a handle to the character mesh and call the appropriate functions. Now we need to define the respawn function for our character; this will be quite simple for now, as the main respawn functionality will take place in the ANSGameMode object, functionality we have yet to specify. For now, add the following definition:

void ANSCharacter::Respawn()
{
    if (Role == ROLE_Authority)
    {
        // Get Location from game mode
        NSPlayerState->Health = 100.0f;
            /* Cast<ANSGameMode>(GetWorld()->
            GetAuthGameMode())->Respawn(this); */
        Destroy(true, true);
    }
}

Here we are simply checking that this function is being executed on the authority instance, then destroying the current actor. As you can see, there is a commented line of code invoking the respawn function on the game mode. We will be defining this function later and un-commenting this line.

Cleaning up shop

The last few methods we have to define are the GetNSPlayerState(), SetNSPlayerState(), and PossessedBy() methods. Let's start with PossessedBy():

void ANSCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);

    NSPlayerState = Cast<ANSPlayerState>(PlayerState);

    if (Role == ROLE_Authority && NSPlayerState != nullptr)
    {
        NSPlayerState->Health = 100.0f;
    }
}

All we are doing here is ensuring that when a character is possessed by a new controller, we get a handle to the ANSPlayerState. For this to work, we must also ensure we specify that ANSPlayerState is the default player state class for this project. I will cover how we do this in the game mode later. If we are the authority and there is a successful cast to the ANSPlayerState class, we also set the player health to be 100.0f. Now the GetNSPlayerState() definition:

ANSPlayerState* ANSCharacter::GetNSPlayerState()
{
    if (NSPlayerState)
    {
        return NSPlayerState;
    }
    else
    {
        NSPlayerState = Cast<ANSPlayerState>(PlayerState);
        return NSPlayerState;
    }
}

This simply returns the ANSPlayerState if it is not null. If it is, we are to cast the current player state handle (replicated to all AActors) to an ANSPlayerState. Lastly, we have the setter for the ANSPlayerState; it appears as follows:

void ANSCharacter::SetNSPlayerState(ANSPlayerState* newPS)
{
    // Ensure PS is valid and only set on server
    if (newPS && Role == ROLE_Authority)
    {
        NSPlayerState = newPS;
        PlayerState = newPS;
    }
}

Compile and run our code! You should encounter no build errors or warnings.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.131.152.166