In the previous post we have created a basic networking behavior that reduces the health and bomb count of the character that presses the Throw Bomb input. In this final part, we’re going to add the spawn functionality for the bomb and configure our code so every character is damaged if he’s inside the explosion radius of the spawned bomb.

Before we start editing our existing code, here is the end result:

The final functionality will be the following:

If the character has any bombs left, when he presses the Throw Bomb input, a bomb will get spawned

When the bomb bounces on the ground, we’re going to “arm” it

After a certain amount of time has passed since the bomb is armed, an explosion will take place

If any player is inside the explosion radius, he will take damage

Creating the Bomb class

The bomb actor constists of:

A static mesh

A projectile movement component (we’re going to use that in order to register the Bounce logic)

A sphere collision component (the projectile movement component works when a collision component is the root of our actor)

Create a class that inherits the Actor class and name it Bomb. Then, add the following header before the .generated.h file:

#include "GameFramework/ProjectileMovementComponent.h"

When you’re done with that, add the following declarations:

Bomb Actor Header file public: // Sets default values for this actor's properties ABomb(); // Called when the game starts or when spawned virtual void BeginPlay() override; protected: /** The static mesh of the comp */ UPROPERTY(VisibleAnywhere) UStaticMeshComponent* SM; /** The projectile movement comp */ UPROPERTY(VisibleAnywhere) UProjectileMovementComponent* ProjectileMovementComp; /** Sphere comp used for collision. Movement component need a collision component as root to function properly */ UPROPERTY(VisibleAnywhere) USphereComponent* SphereComp; /** The delay until explosion */ UPROPERTY(EditAnywhere, Category = BombProps) float FuseTime = 2.5f; UPROPERTY(EditAnywhere, Category = BombProps) float ExplosionRadius = 200.f; UPROPERTY(EditAnywhere, Category = BombProps) float ExplosionDamage = 25.f; /** The particle system of the explosion */ UPROPERTY(EditAnywhere) UParticleSystem* ExplosionFX; private: /** Marks the properties we wish to replicate */ virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; UPROPERTY(ReplicatedUsing = OnRep_IsArmed) bool bIsArmed = false; /** Called when bIsArmed gets updated */ UFUNCTION() void OnRep_IsArmed(); /** Arms the bomb for explosion */ void ArmBomb(); /** Called when our bomb bounces */ UFUNCTION() void OnProjectileBounce(const FHitResult& ImpactResult, const FVector& ImpactVelocity); /** Performs an explosion after a certain amount of time */ void PerformDelayedExplosion(float ExplosionDelay); /** Performs an explosion when called */ UFUNCTION() void Explode(); //Simulate explosion functions /** * The multicast specifier, indicates that every client will call the SimulateExplosionFX_Implementation. * You don't have to generate an implementation for this function. */ UFUNCTION(Reliable, NetMulticast) void SimulateExplosionFX(); /** The actual implementation of the SimulateExplosionFX */ void SimulateExplosionFX_Implementation(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public : // Sets default values for this actor's properties ABomb ( ) ; // Called when the game starts or when spawned virtual void BeginPlay ( ) override ; protected : /** The static mesh of the comp */ UPROPERTY ( VisibleAnywhere ) UStaticMeshComponent * SM ; /** The projectile movement comp */ UPROPERTY ( VisibleAnywhere ) UProjectileMovementComponent * ProjectileMovementComp ; /** Sphere comp used for collision. Movement component need a collision component as root to function properly */ UPROPERTY ( VisibleAnywhere ) USphereComponent * SphereComp ; /** The delay until explosion */ UPROPERTY ( EditAnywhere , Category = BombProps ) float FuseTime = 2.5f ; UPROPERTY ( EditAnywhere , Category = BombProps ) float ExplosionRadius = 200.f ; UPROPERTY ( EditAnywhere , Category = BombProps ) float ExplosionDamage = 25.f ; /** The particle system of the explosion */ UPROPERTY ( EditAnywhere ) UParticleSystem * ExplosionFX ; private : /** Marks the properties we wish to replicate */ virtual void GetLifetimeReplicatedProps ( TArray < FLifetimeProperty > & OutLifetimeProps ) const override ; UPROPERTY ( ReplicatedUsing = OnRep_IsArmed ) bool bIsArmed = false ; /** Called when bIsArmed gets updated */ UFUNCTION ( ) void OnRep_IsArmed ( ) ; /** Arms the bomb for explosion */ void ArmBomb ( ) ; /** Called when our bomb bounces */ UFUNCTION ( ) void OnProjectileBounce ( const FHitResult & ImpactResult , const FVector & ImpactVelocity ) ; /** Performs an explosion after a certain amount of time */ void PerformDelayedExplosion ( float ExplosionDelay ) ; /** Performs an explosion when called */ UFUNCTION ( ) void Explode ( ) ; //Simulate explosion functions /** * The multicast specifier, indicates that every client will call the SimulateExplosionFX_Implementation. * You don't have to generate an implementation for this function. */ UFUNCTION ( Reliable , NetMulticast ) void SimulateExplosionFX ( ) ; /** The actual implementation of the SimulateExplosionFX */ void SimulateExplosionFX_Implementation ( ) ;

The reason we’re using a multicast function is because the SimulateExplosionFX essentially provides some “eye-candy” for our game. Since that isn’t important in terms of gameplay, it’s safe for every client to call his own implementation. Moreover, you can omit the reliable specifier in order to avoid any bottlenecks in case of bad internet connection. It’s up to you to decide what’s really important for your game and mark it as reliable.

Moving on to the logic for our Bomb class:

Bomb source file // Sets default values ABomb::ABomb() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; SphereComp = CreateDefaultSubobject<USphereComponent>(FName("SphereComp")); SetRootComponent(SphereComp); SM = CreateDefaultSubobject<UStaticMeshComponent>(FName("SM")); SM->SetupAttachment(SphereComp); ProjectileMovementComp = CreateDefaultSubobject<UProjectileMovementComponent>(FName("ProjectileMovementComp")); ProjectileMovementComp->bShouldBounce = true; //Since we need to replicate some functionality //for this actor, we need to mark it as true SetReplicates(true); } // Called when the game starts or when spawned void ABomb::BeginPlay() { Super::BeginPlay(); //Register that function that will be called in any bounce event ProjectileMovementComp->OnProjectileBounce.AddDynamic(this, &ABomb::OnProjectileBounce); } void ABomb::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //Tell the engine that we wish to replicate the bIsArmed variable DOREPLIFETIME(ABomb, bIsArmed); } void ABomb::ArmBomb() { if (bIsArmed) { //Chance the base color of the static mesh to red UMaterialInstanceDynamic* DynamicMAT = SM->CreateAndSetMaterialInstanceDynamic(0); DynamicMAT->SetVectorParameterValue(FName("Color"), FLinearColor::Red); } } void ABomb::OnProjectileBounce(const FHitResult& ImpactResult, const FVector& ImpactVelocity) { //If the bomb is not armed and we have authority, //arm it and perform a delayed explosion if (!bIsArmed && Role == ROLE_Authority) { bIsArmed = true; ArmBomb(); PerformDelayedExplosion(FuseTime); } } void ABomb::OnRep_IsArmed() { //Will get called when the bomb is armed //from the authority client if (bIsArmed) { ArmBomb(); } } void ABomb::PerformDelayedExplosion(float ExplosionDelay) { FTimerHandle TimerHandle; FTimerDelegate TimerDel; TimerDel.BindUFunction(this, FName("Explode")); GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDel, ExplosionDelay, false); } void ABomb::Explode() { SimulateExplosionFX(); //We won't use any specific damage types in our case TSubclassOf<UDamageType> DmgType; //Do not ignore any actors TArray<AActor*> IgnoreActors; //This will eventually call the TakeDamage function that we have overriden in the Character class UGameplayStatics::ApplyRadialDamage(GetWorld(), ExplosionDamage, GetActorLocation(), ExplosionRadius, DmgType, IgnoreActors, this, GetInstigatorController()); FTimerHandle TimerHandle; FTimerDelegate TimerDel; TimerDel.BindLambda([&]() { Destroy(); }); //Destroy the actor after 0.3 seconds. GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDel, 0.3f, false); } void ABomb::SimulateExplosionFX_Implementation() { if (ExplosionFX) { UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionFX, GetTransform(), true); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 // Sets default values ABomb :: ABomb ( ) { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick . bCanEverTick = true ; SphereComp = CreateDefaultSubobject < USphereComponent > ( FName ( "SphereComp" ) ) ; SetRootComponent ( SphereComp ) ; SM = CreateDefaultSubobject < UStaticMeshComponent > ( FName ( "SM" ) ) ; SM -> SetupAttachment ( SphereComp ) ; ProjectileMovementComp = CreateDefaultSubobject < UProjectileMovementComponent > ( FName ( "ProjectileMovementComp" ) ) ; ProjectileMovementComp -> bShouldBounce = true ; //Since we need to replicate some functionality //for this actor, we need to mark it as true SetReplicates ( true ) ; } // Called when the game starts or when spawned void ABomb :: BeginPlay ( ) { Super :: BeginPlay ( ) ; //Register that function that will be called in any bounce event ProjectileMovementComp -> OnProjectileBounce . AddDynamic ( this , &ABomb :: OnProjectileBounce ) ; } void ABomb :: GetLifetimeReplicatedProps ( TArray < FLifetimeProperty > & OutLifetimeProps ) const { Super :: GetLifetimeReplicatedProps ( OutLifetimeProps ) ; //Tell the engine that we wish to replicate the bIsArmed variable DOREPLIFETIME ( ABomb , bIsArmed ) ; } void ABomb :: ArmBomb ( ) { if ( bIsArmed ) { //Chance the base color of the static mesh to red UMaterialInstanceDynamic * DynamicMAT = SM -> CreateAndSetMaterialInstanceDynamic ( 0 ) ; DynamicMAT -> SetVectorParameterValue ( FName ( "Color" ) , FLinearColor :: Red ) ; } } void ABomb :: OnProjectileBounce ( const FHitResult & ImpactResult , const FVector & ImpactVelocity ) { //If the bomb is not armed and we have authority, //arm it and perform a delayed explosion if ( ! bIsArmed && Role == ROLE_Authority ) { bIsArmed = true ; ArmBomb ( ) ; PerformDelayedExplosion ( FuseTime ) ; } } void ABomb :: OnRep_IsArmed ( ) { //Will get called when the bomb is armed //from the authority client if ( bIsArmed ) { ArmBomb ( ) ; } } void ABomb :: PerformDelayedExplosion ( float ExplosionDelay ) { FTimerHandle TimerHandle ; FTimerDelegate TimerDel ; TimerDel . BindUFunction ( this , FName ( "Explode" ) ) ; GetWorld ( ) -> GetTimerManager ( ) . SetTimer ( TimerHandle , TimerDel , ExplosionDelay , false ) ; } void ABomb :: Explode ( ) { SimulateExplosionFX ( ) ; //We won't use any specific damage types in our case TSubclassOf < UDamageType > DmgType ; //Do not ignore any actors TArray < AActor * > IgnoreActors ; //This will eventually call the TakeDamage function that we have overriden in the Character class UGameplayStatics :: ApplyRadialDamage ( GetWorld ( ) , ExplosionDamage , GetActorLocation ( ) , ExplosionRadius , DmgType , IgnoreActors , this , GetInstigatorController ( ) ) ; FTimerHandle TimerHandle ; FTimerDelegate TimerDel ; TimerDel . BindLambda ( [ & ] ( ) { Destroy ( ) ; } ) ; //Destroy the actor after 0.3 seconds. GetWorld ( ) -> GetTimerManager ( ) . SetTimer ( TimerHandle , TimerDel , 0.3f , false ) ; } void ABomb :: SimulateExplosionFX_Implementation ( ) { if ( ExplosionFX ) { UGameplayStatics :: SpawnEmitterAtLocation ( GetWorld ( ) , ExplosionFX , GetTransform ( ) , true ) ; } }

The last things we need in order to test our functionality is modify the code we’ve written in our character class in the previous section. Specifically, we’re going to add the actual logic for the bomb spawn and we’re going to remove the hardcoded take damage function calls.

Open up the header file of your character and add the following library before the .generated.h file:

#include "Bomb.h"

Then, declare a subclass property of our bomb:

Bomb BP ref public: /** Bomb Blueprint */ UPROPERTY(EditAnywhere) TSubclassOf<ABomb> BombActorBP; 1 2 3 4 public : /** Bomb Blueprint */ UPROPERTY ( EditAnywhere ) TSubclassOf < ABomb > BombActorBP ;

As a last step, modify the AttempToSpawnBomb and SpawnBomb functions to match the following functionality:

AttempToSpawnBomb and SpawnBomb implementations void ANetworkingTutCharacter::AttempToSpawnBomb() { if (HasBombs()) { if (Role < ROLE_Authority) { ServerSpawnBomb(); } else SpawnBomb(); } } void ANetworkingTutCharacter::SpawnBomb() { //Decrease the bomb count and update the text in the local client //OnRep_BombCount will be called in every other client BombCount--; UpdateCharText(); FActorSpawnParameters SpawnParameters; SpawnParameters.Instigator = this; SpawnParameters.Owner = GetController(); //Spawn the bomb GetWorld()->SpawnActor<ABomb>(BombActorBP, GetActorLocation() + GetActorForwardVector() * 200, GetActorRotation(),SpawnParameters); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void ANetworkingTutCharacter :: AttempToSpawnBomb ( ) { if ( HasBombs ( ) ) { if ( Role < ROLE_Authority ) { ServerSpawnBomb ( ) ; } else SpawnBomb ( ) ; } } void ANetworkingTutCharacter :: SpawnBomb ( ) { //Decrease the bomb count and update the text in the local client //OnRep_BombCount will be called in every other client BombCount -- ; UpdateCharText ( ) ; FActorSpawnParameters SpawnParameters ; SpawnParameters . Instigator = this ; SpawnParameters . Owner = GetController ( ) ; //Spawn the bomb GetWorld ( ) -> SpawnActor < ABomb > ( BombActorBP , GetActorLocation ( ) + GetActorForwardVector ( ) * 200 , GetActorRotation ( ) , SpawnParameters ) ; }

Save and compile your code. Create a Blueprint based on your bomb and assign the Shape Sphere to your bomb’s static mesh. Moreover, make sure that your Sphere Component covers the whole static mesh (in my case the desired sphere radius equals to 55 units) and has a valid Collision Profile assigned to it (ie Projectile / BlockAll). Moreover, assign a valid explosion particle system to your Blueprint (I’ve used the P_Explosion that comes in with the starter content).

Then, make sure to reference your Bomb Blueprint to your Character’s blueprint and test your functionality!