Unreal Engine - Network Replication (2024)
- Vichakshana Arangala
- Jun 27, 2025
- 4 min read

This is a demo for a Multiplayer Roguelike developed in Unreal Engine using Unreal’s Paragon Asset Pack Character Gideon. The development of this project was focused on Network Replication. This has been achieved through a custom Ability System that is replicated properly on both Client and Server. The game was made entirely in C++.

In a standard a combat encounter, once an enemy has spotted Gideon, a question mark would light up above their heads, reminiscent of Metal Gear Solid. Both the Enemy and the Player are made up of modular components that allow them to implement similar behavior such as getting burned.

The Dash is one of Gideon’s many special abilities, allowing Gideon to speed across the map and avoid enemies.

Gideon gains Rage when he is attacked. And once Gideon’s Rage has reach 50%, he can send out a devastating blackhole that sucks up everything.

Gideon can also teleport across the map to quickly escape Danger.

All of Gideon’s abilities are replicated properly across both Client and Server in the most optimal way possible.
NOTE: Enemies were disable for this recording.
Behind Enemy Lines...
AI Character Character Overview// Sets default valuesASHAICharacter::ASHAICharacter(){ PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp"); AttributeComp = CreateDefaultSubobject<USHAttributeComponent>("AttributeComp"); AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned; ActionComp = CreateDefaultSubobject<USHActionComponent>("ActionComp"); //GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Ignore); GetMesh()->SetGenerateOverlapEvents(true); // Enabled on mesh to react to incoming projectiles USkeletalMeshComponent* SkelMesh = GetMesh(); SkelMesh->SetGenerateOverlapEvents(true); // Skip performing overlap queries on the Physics Asset after animation (17 queries in case of our MinionRangedBP) SkelMesh->bUpdateOverlapsOnAnimationFinalize = false; // Skip bones when not visible, may miss anim notifies etc. if animation is skipped so these options must be tested per use case SkelMesh->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered;}void ASHAICharacter::PostInitializeComponents(){ Super::PostInitializeComponents(); PawnSensingComp->OnSeePawn.AddDynamic(this, &ASHAICharacter::OnPawnSeen); AttributeComp->OnHealthChanged.AddDynamic(this, &ASHAICharacter::OnHealthChanged);}void ASHAICharacter::OnHealthChanged(AActor* InstigatorActor, USHAttributeComponent* OwningComp, float newHealth, float Delta){ if (Delta < 0.0f) //make sure we didn't heal or anything { if (InstigatorActor != this) { SetTargetActor(InstigatorActor); } if (ActiveHealthBar == nullptr) { ActiveHealthBar = CreateWidget<USHWorldUserWidget>(GetWorld(), HealthBarWidgetClass); if (ActiveHealthBar) { ActiveHealthBar->AttachedActor = this; ActiveHealthBar->AddToViewport(); } } GetMesh()->SetScalarParameterValueOnMaterials(TimeToHitParamName, GetWorld()->TimeSeconds); //Died if (newHealth <= 0.0f) { //Just died - stop behaviour tree, enable ragdoll , set lifespan(how long till we call destroy actor) //Sopping logic AAIController* AIC = Cast<AAIController>(GetController()); if (ensureAlways(AIC)) { AIC->GetBrainComponent()->StopLogic("Killed"); } //Ragdoll GetMesh()->SetAllBodiesSimulatePhysics(true); GetMesh()->SetCollisionProfileName("Ragdoll"); GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision); GetCharacterMovement()->DisableMovement(); //Destroy actor in 10 seconds SetLifeSpan(10.0f); } }}void ASHAICharacter::SetTargetActor(AActor* NewTarget){ AAIController* AIC = Cast<AAIController>(GetController()); if (AIC) { AIC->GetBlackboardComponent()->SetValueAsObject("TargetActor", NewTarget); }}AActor* ASHAICharacter::GetTargetActor() const{ AAIController* AIC = GetController<AAIController>(); return Cast<AActor>(AIC->GetBlackboardComponent()->GetValueAsObject("TargetActor"));}void ASHAICharacter::OnPawnSeen(APawn* Pawn){ //Ignore if target is already set if (GetTargetActor() != Pawn) { SetTargetActor(Pawn); //DrawDebugString(GetWorld(), GetActorLocation(), "PLAYER SPOTTED", nullptr, FColor::White, 4.0f, true); MulticastPawnSeen(); }}void ASHAICharacter::MulticastPawnSeen_Implementation(){ USHWorldUserWidget* NewWidget = CreateWidget<USHWorldUserWidget>(GetWorld(), SpottedWidgetClass); if (NewWidget) { NewWidget->AttachedActor = this; //Index of 10 (or anything higher than default of 0) places this on top of any other widget. //May end up being minion health bar otherwise. NewWidget->AddToViewport(10); }}Ranged Attack - Behaviour Tree TaskUSHBTTask_RangedAttack::USHBTTask_RangedAttack(){ MuzzleSocket = "Muzzle_01"; MaxBulletSpread = 2.0f;}EBTNodeResult::Type USHBTTask_RangedAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory){ AAIController* MyController = OwnerComp.GetAIOwner(); if (ensureAlways(MyController)) { ACharacter* MyPawn = Cast<ACharacter>(MyController->GetPawn()); if (MyPawn == nullptr) { return EBTNodeResult::Failed; } FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation(MuzzleSocket); AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor")); if (TargetActor == nullptr) { return EBTNodeResult::Failed; } if (!USHAttributeComponent::IsActorAlive(TargetActor)) { return EBTNodeResult::Failed; } FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation; FRotator MuzzleRotation = Direction.Rotation(); MuzzleRotation.Pitch += FMath::RandRange(0.0f, MaxBulletSpread); MuzzleRotation.Yaw += FMath::RandRange(-MaxBulletSpread, MaxBulletSpread); FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; Params.Instigator = MyPawn; AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, Params); return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed; } return EBTNodeResult::Failed;}Check Health - Behaviour Tree ServiceUSHBTService_CheckHealth::USHBTService_CheckHealth(){ LowHealthFraction = 0.3f;}void USHBTService_CheckHealth::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds){ Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds); APawn* AIPawn = OwnerComp.GetAIOwner()->GetPawn(); if (ensure(AIPawn)) { USHAttributeComponent* AttributeComp = USHAttributeComponent::GetAttributes(AIPawn); if (ensure(AttributeComp)) { bool bLowHealth = (AttributeComp->GetHealth() / AttributeComp->GetHealthMax()) < LowHealthFraction; UBlackboardComponent* BlackBoardComp = OwnerComp.GetBlackboardComponent(); BlackBoardComp->SetValueAsBool(LowHealthKey.SelectedKeyName, bLowHealth); } }}Heal Self - Behaviour Tree TaskEBTNodeResult::Type USHBTTask_HealSelf::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory){ APawn* MyPawn = Cast<APawn>(OwnerComp.GetAIOwner()->GetPawn()); if (MyPawn == nullptr) { return EBTNodeResult::Failed; } USHAttributeComponent* AttributeComp = USHAttributeComponent::GetAttributes(MyPawn); if (ensure(AttributeComp)) { AttributeComp->ApplyHealthChange(MyPawn, AttributeComp->GetHealthMax()); } return EBTNodeResult::Succeeded;}Putting it all together...

Once an enemy has spawned, it will decide it’s movement based on the its health, and if it’s in attack range.

If it has low health, it will find a hiding spot, move to said spot and heal itself before engaging in combat again.

Once it’s in attack range, it will attack upto 3 times before deciding on its next action.

If it’s not in attack range or if it has already attacked, the enemy will then move to a random location, wait for 5 seconds, and then go through its motions once more.
The end result...
What I learned....
The goal of this exercise was to teach myself Unreal Engine Network replication, and that much was achieved. This project gave me a solid understanding of Unreal Engine's Network replication framework.
Future Plans
The experience of this project can be easily transferred over to a number of Unreal Engine projects, and I plan on using this knowledge to create a dedicated Unreal Engine Multiplayer game.

Comments