Mukund Pareek / mukund pareek
  • Projects
  • Blog
  • Notes
  • Resume

MP Mukund Pareek

Unreal Engine C++ Developer

  • Projects
  • Blog
  • Notes
  • Resume

© 2026 Mukund Pareek. Built with Next.js and Tailwind CSS.

Projects
February 15, 2026
Gameplay Systems

Quick Facts

Engine
Unreal Engine 5
Language
C++
System
Gameplay Ability System
Type
Gameplay Systems
Status
Prototype
Body and Soul - Replicated Co-op Health Prototype thumbnail
Gameplay Systems

Body and Soul - Replicated Co-op Health Prototype

Asymmetric co-op prototype in Unreal Engine 5 where players spawn as Body/Soul roles and share a replicated team-health pool. Built with C++, GAS integration, and server-authoritative multiplayer replication.

unreal
gas
multiplayer
replication
c++
coop

TL;DR

Multiplayer co-op prototype in Unreal Engine 5 where players alternately spawn as ABodyCharacter or ASoulCharacter and draw from a single replicated team-health value. GAS is integrated for ability and attribute foundations - using the PlayerState-owned ASC pattern for players and a pawn-owned ASC for enemies. Shared match health is managed by ABnSGameState for deterministic server authority, keeping GAS evolution risk separate from co-op fail-state reliability.

Role & Scope

  • Role: Solo gameplay systems programmer
  • Duration: 2 weeks (prototype sprint)
  • Responsibilities: Spawn-role architecture, GAS initialization patterns, shared-health replication, prototype UI feedback, multiplayer PIE validation

Problem

The co-op design goal was: two players, one team life pool.

The challenge was implementing this quickly while still laying groundwork for deeper GAS-driven expansion. Rather than immediately coupling both players to a shared ASC - which introduces complex multi-avatar ownership edge cases - the prototype uses a deliberate intermediate architecture:

  • GAS per player for character capability scaffolding (abilities, attributes, effects).
  • GameState as a single source of truth for the shared team health value.

This separates ability evolution risk from core co-op fail-state reliability.

Codebase Structure

Source/BodyAndSoul/
├── BodyAndSoulGameMode.*      # alternates spawn classes (Body / Soul)
├── BnSGameState.*             # replicated shared-health authority
├── BnSPlayerState.*           # player-owned ASC + AttributeSet
├── BnSAttributeSet.*          # GAS attributes (Health, MaxHealth) with replication
├── BodyAndSoulCharacter.*     # base character: movement, input, GAS helpers, damage hook
├── PlayerCharacter.*          # possession + OnRep init of ASC from PlayerState
├── BodyCharacter.*            # Body role variant
├── SoulCharacter.*            # Soul role variant
├── EnemyCharacter.*           # enemy with pawn-local ASC + attribute set
├── HealthWidget.*             # shared-health UI
└── ReduceHealthAbility.*      # placeholder gameplay ability

Architecture

Architecture: alternating spawn roles, two GAS ownership patterns, and shared match health via GameState replication.

System Design

Spawn / Role Assignment

ABodyAndSoulGameMode stores FirstPawn and SecondPawn class references and alternates which is returned by GetDefaultPawnClassForController_Implementation. Each joining player is assigned the next role in sequence - Body, Soul, Body, Soul - without a lobby or role-picker.

This keeps role assignment server-authoritative and makes asymmetric co-op testing fast to set up.

GAS Ownership - Two Patterns in One Project

Player path (PlayerState-owned ASC):

  • ABnSPlayerState owns UBnSAbilitySystemComponent and UBnSAttributeSet
  • APlayerCharacter initializes GAS pointers in both PossessedBy (server) and OnRep_PlayerState (client)
  • ABodyAndSoulCharacter exposes shared helpers: GiveDefaultAbilities() and InitDefaultAttributes()

Enemy path (pawn-owned ASC):

  • AEnemyCharacter creates ASC and AttributeSet directly on the character in the constructor
  • Initializes actor info in BeginPlay

Demonstrating both patterns in one project makes the ownership tradeoffs explicit: PlayerState-owned survives possession changes and replication events; pawn-owned is simpler and sufficient for AI-controlled actors.

Shared Health & Damage Flow

Team health lives in ABnSGameState as fPlayerHealth:

  1. ABodyAndSoulCharacter binds overlap callbacks on spawn
  2. On overlap with an actor tagged Damage, the character calls ApplyDamage(1.f) server-side
  3. Server routes to ABnSGameState::ApplyDamageToSharedHealth
  4. fPlayerHealth is clamped and replicated via OnRep_SharedHealth
  5. UHealthWidget reads the updated value and refreshes the progress bar and status text

Centralizing this in GameState gives a single server-authoritative location that any client can read without querying individual player state.

UI Feedback

UHealthWidget reads ABnSGameState::fPlayerHealth and updates:

  • Progress bar fill percentage
  • Status text: "You are damaged!" / "You are dead!"

Current implementation uses a button-triggered update path for prototype validation. The step to event-driven UI is a delegate binding once the core replication is proven stable.

Code Snippet

cppPlayerCharacter.cpp
// GAS must be initialized on both server and client paths.
// PossessedBy fires on the server when the controller takes possession.
// OnRep_PlayerState fires on the client when PlayerState replicates.
// Both paths are required - skipping either leaves ASC uninitialized on one side.

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

  ABnSPlayerState* PS = GetPlayerState<ABnSPlayerState>();
  if (!PS) return;

  AbilitySystemComponent = PS->GetAbilitySystemComponent();
  AbilitySystemComponent->InitAbilityActorInfo(PS, this);

  AttributeSet = PS->GetAttributeSet();

  // Grant starting abilities and initialize attribute defaults (server only).
  GiveDefaultAbilities();
  InitDefaultAttributes();
}

void APlayerCharacter::OnRep_PlayerState()
{
  Super::OnRep_PlayerState();

  // Client-side init - PlayerState may not exist until this rep fires.
  ABnSPlayerState* PS = GetPlayerState<ABnSPlayerState>();
  if (!PS) return;

  AbilitySystemComponent = PS->GetAbilitySystemComponent();
  AbilitySystemComponent->InitAbilityActorInfo(PS, this);

  AttributeSet = PS->GetAttributeSet();
}

Video Demo

Demo: alternating Body/Soul spawn, triggering a damage overlap, and observing the replicated shared health update across PIE instances.

Challenges & Solutions

Challenge 1: GAS health vs. shared match health

Issue: The prototype needed both GAS attribute infrastructure and a co-op team health pool. Routing all damage through GAS Gameplay Effects to a shared attribute on a manager actor introduces multi-avatar ASC ownership complexity that is risky to get right in a short sprint.

Solution: Keep GAS attributes per player and per enemy for ability/effect scaffolding. Route the shared fail-state health through ABnSGameState where server authority is trivial to enforce and replication is straightforward. The two concerns evolve independently.

Challenge 2: ASC initialization timing in networked play

Issue: On clients, ABnSPlayerState is not guaranteed to exist at the moment BeginPlay or PossessedBy fires. Initializing ASC pointers only in PossessedBy left clients with null references.

Solution: Initialize in both PossessedBy (server) and OnRep_PlayerState (client) inside APlayerCharacter. The server path handles authority setup and ability granting; the client path ensures GAS pointers are valid as soon as the PlayerState replicates.

Challenge 3: Keeping prototype UI testable without event-driven bindings

Issue: Wiring full delegate-based UI updates early adds complexity before the underlying replication is confirmed stable.

Solution: Use a button-triggered update and direct GameState reads during prototyping. This gives immediate PIE multiplayer visibility into shared-health state changes and makes the underlying logic easy to step through before adding event-driven polish.

Replication Notes

  • ABnSGameState::fPlayerHealth - declared with ReplicatedUsing=OnRep_SharedHealth; registered in GetLifetimeReplicatedProps
  • UBnSAttributeSet - Health and MaxHealth replicated with UPROPERTY(ReplicatedUsing=...) and rep-notify macros
  • Shared health mutation is server-gated (HasAuthority()) in ABnSGameState

Current Gaps

  • UReduceHealthAbility exists as a placeholder - the ability body is not yet implemented
  • USharedHealth UObject exists in the source but is not the active authority path; ABnSGameState holds that role
  • UI update is button-triggered, not event-driven

What I Would Change Next

  1. Move shared health fully into GAS via a dedicated authority actor with its own ASC, replacing the float in GameState
  2. Replace the button-triggered UI path with delegate/event-driven binding against the replicated value
  3. Add explicit death-state replication and synchronized animation triggers on both characters
  4. Add automated multiplayer functional tests covering damage application and death parity across clients
  5. Add reconnect/disconnect handling for shared-state ownership transitions