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
January 10, 2026
Gameplay Systems

Quick Facts

Engine
Unreal Engine 5
Language
C++
System
Gameplay Ability System
Type
Gameplay Systems
Status
Prototype
Lyra FPS Prototype thumbnail
Gameplay Systems

Lyra FPS Prototype

Team Deathmatch prototype built on Lyra-inspired architecture in Unreal Engine C++ - experience-driven pawn setup, GAS shoot/death abilities, replicated health with delegate-driven HUD updates, and a full death/respawn/scoring loop.

unreal
lyra
gas
fps
c++

TL;DR

A custom Lyra-inspired Team Deathmatch prototype built in Unreal Engine C++. The project is not a modification of the Lyra sample - it reimplements Lyra's architectural patterns from scratch using the same design philosophy:

  • Experience asset selected in GameMode and loaded asynchronously through a replicated manager
  • PawnData and AbilitySet assets applied through a PawnExtension init-state pipeline
  • Abilities granted server-side with input tags; blocked by gameplay tags during death/invincibility
  • Health changes propagate to HUD through delegate binding at DataInitialized state
  • Death, respawn, team scoring, and spawn invincibility enforced through GAS + GameMode orchestration

The engineering focus is initialization correctness and replication timing across the full match lifecycle.

Role & Scope

  • Role: Solo gameplay systems engineer
  • Duration: 3 weeks
  • Engine/Stack: Unreal Engine C++, GAS, ModularGameplay, team subsystem, UMG

Problem

Lyra-style modular systems are powerful but timing-sensitive. The hard part was not implementing a line trace - it was making every system become ready in the correct order across server and client:

  • Experience ID replication and async load completion
  • PawnData application and ability grants (server authority)
  • InitAbilityActorInfo on every respawn
  • Health/UI binding only after DataInitialized state

Without strict gating these produce race conditions: missing abilities on early spawn windows, stale HUD values, and input lock lingering through death transitions.

Architecture

Runtime architecture: experience load, PawnData/AbilitySet grant pipeline, shoot/damage/death/respawn loop, and HUD update path.

Startup flow - game boot to first shot

  1. GameMode::InitGame parses ?ExperienceId= (fallback: UZ_GameMode_TeamDeathmatch)
  2. ExperienceManagerComponent replicates ExperienceId; both server and client async-load the experience asset
  3. Pawn is spawned with PawnData from the experience definition
  4. UUZ_PawnExtensionComponent advances through Spawned → DataAvailable → DataInitialized → GameplayReady
  5. On DataInitialized (server): abilities granted, input tags injected into spec source tags, handles stored
  6. UUZ_HealthComponent binds to ASC health delegate at DataInitialized; broadcasts current value immediately
  7. Player presses fire; GAS validates tags, cooldown, and invincibility; line trace executes server-side
  8. Damage GE applied; health zero triggers death ability; GameMode increments score and starts respawn timer

Implementation Breakdown

UUZ_ExperienceManagerComponent - async load and replication

  • Server parses ?ExperienceId= in GameMode::InitGame and calls SetCurrentExperienceId
  • Component replicates ExperienceId; clients begin async load from OnRep_ExperienceId
  • Load state machine (Unloaded → Loading → Loaded) prevents duplicate load attempts
  • CallOrRegister_OnExperienceLoaded() allows any system to safely defer its setup behind experience readiness

UUZ_PawnExtensionComponent - init-state pipeline

The core Lyra-style initialization gate. Implements IGameFrameworkInitStateInterface with a four-state chain:

StateGate condition
SpawnedActor exists
DataAvailablePawnData != nullptr
DataInitializedValid ASC + avatar actor match
GameplayReadyAbilities granted (server); replicated ability state (client)

Ability grant flow (server, at DataInitialized):

  • Reads PawnData->AbilitySetIds
  • Sync-loads each UUZ_AbilitySet
  • Grants via ASC->GiveAbility(FGameplayAbilitySpec)
  • Injects input tags into DynamicSpecSourceTags
  • Stores FUZ_AbilitySet_GrantedHandles for teardown cleanup

AUZ_PlayerState - ASC and attribute host

PlayerState owns the ASC (Mixed replication mode) and UUZ_HealthSet. Placing ASC here improves continuity across pawn death/respawn and keeps attribute ownership stable. InitializeAbilitySystem(APawn*) calls InitAbilityActorInfo(this, InPawn), resets health to max, and forwards ASC into the PawnExtension pipeline.

UUZ_GameplayAbility_Shoot

  • Activated by TAG_Input_Shoot; blocked by TAG_Gameplay_Dead
  • Server-only execution policy
  • Cooldown GE (UZ_GE_ShootCooldown) gates fire rate
  • Server line trace against WorldStatic/WorldDynamic/Pawn channels
  • Friendly-fire validated through UUZ_TeamSubsystem::CanCauseDamage
  • Invincibility tag check before applying damage
  • Applies UZ_GE_Damage to target ASC
  • Ends immediately (single-shot model)

UUZ_GameplayAbility_Death

  • Activated when health reaches zero
  • Applies owned tags while active: TAG_Gameplay_Dead, TAG_Gameplay_MovementStopped
  • Cancels shoot ability; disables move/look input
  • Resolves killer actor from UUZ_HealthComponent::LastKillerActor
  • Notifies GameMode for score increment + respawn scheduling
  • Intentionally stays active until GameMode cancels it on respawn to hold the dead state

UUZ_HealthComponent - binding timing

  • Subscribes to PawnExtension init-state changes
  • Binds to ASC health delegate only at DataInitialized - never earlier
  • Stores delegate handle for clean unbind on EndPlay
  • Broadcasts initial health value immediately after binding so the HUD is never empty on first frame
  • On server: reacts to OnOutOfHealth by activating death ability

AUZ_PlayerController - HUD connection

  • Rebinds on every SetPawn (covers respawn)
  • Binds to HealthComponent::OnHealthChanged
  • Fires OnHealthValueChanged immediately if attributes are already bound at bind time
  • Covers the spawn/respawn timing variation where delegate registration order differs by net role

Team subsystem and scoring

  • UUZ_TeamCreationComponent spawns team info actors at match start
  • AUZ_PlayerState replicates team ID with change delegates
  • UUZ_TeamSubsystem::CanCauseDamage prevents friendly fire at the shoot ability level
  • Death ability reports killer to GameMode; GameMode increments killer team score
  • Score-limit check triggers match-end; dead pawn destroyed; respawn timer scheduled; CancelAbilitiesByTag(Dead) then RestartPlayer

Code Snippet

cppUUZ_GameplayAbility_Shoot.cpp
void UUZ_GameplayAbility_Shoot::ActivateAbility(
  const FGameplayAbilitySpecHandle Handle,
  const FGameplayAbilityActorInfo* ActorInfo,
  const FGameplayAbilityActivationInfo ActivationInfo,
  const FGameplayEventData* TriggerEventData)
{
  // CommitAbility validates tags, cooldown, and cost; returns false if any fail.
  if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
  {
      EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
      return;
  }

  // Line trace - server only (execution policy: ServerOnly).
  FHitResult Hit;
  const bool bHit = PerformLineTrace(ActorInfo, Hit);

  if (bHit && Hit.GetActor())
  {
      // Friendly-fire check through team subsystem.
      UUZ_TeamSubsystem* Teams = GetWorld()->GetSubsystem<UUZ_TeamSubsystem>();
      if (Teams && Teams->CanCauseDamage(ActorInfo->OwnerActor.Get(), Hit.GetActor()))
      {
          // Invincibility check before applying damage.
          UAbilitySystemComponent* TargetASC =
              UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Hit.GetActor());

          if (TargetASC && !TargetASC->HasMatchingGameplayTag(TAG_Gameplay_Invincible))
          {
              FGameplayEffectSpecHandle DamageSpec =
                  MakeOutgoingGameplayEffectSpec(DamageEffectClass, GetAbilityLevel());
              ApplyGameplayEffectSpecToTarget(Handle, ActorInfo, ActivationInfo,
                  DamageSpec, FGameplayAbilityTargetDataHandle());
          }
      }
  }

  EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
}

Video Demo

Demo: shooting, taking damage, health HUD updates, death, and respawn with score tracking.

Network & Replication

  • PlayerState net update frequency raised to 100 Hz - reduces sluggish remote health updates on default tick rates
  • UUZ_HealthSet::Health replicated with COND_None (all observers need it for damage feedback)
  • Ability grants are server-only; no client race on grant timing
  • Shoot and death abilities run server-side - no client divergence in damage or state transitions
  • GameInstance explicitly registers init-state tags in progression order for at-or-past comparisons

Challenges & Solutions

Challenge 1: HUD desync after spawn/respawn

Root cause: HUD binding could execute before the health delegate was registered, or the first frame value was never pushed.

Fix: Bind at DataInitialized init-state + immediately broadcast current health value on bind + controller re-binds on SetPawn with an immediate callback if already bound. All three are required to handle the timing variation across net roles.

Challenge 2: Abilities absent on early spawn window

Root cause: Ability grant pipeline and pawn readiness converged at different times depending on network mode. Input presses during the gap found no ability specs.

Fix: Gate readiness using the PawnExtension four-state chain with server authority checks. Only mark GameplayReady after grants complete on server; clients advance via replicated ability state.

Challenge 3: Dead-state tags lingering after respawn

Root cause: Death ability applied TAG_Gameplay_Dead and input locks. Without explicit cleanup, these persisted through pawn destruction and restart.

Fix: Keep death ability active by design through the respawn transition. GameMode cancels it by tag (CancelAbilitiesByTag) before calling RestartPlayer, ensuring clean transition from dead to alive state.

Scope - Implemented vs Planned

Implemented:

  • Experience loading pipeline with replicated ID and async asset load
  • PawnData-driven ability set grant pipeline with handle cleanup
  • Shoot ability: cooldown GE, line trace, friendly-fire + invincibility checks, damage GE
  • Health attribute replication and delegate-driven HUD update path
  • Death ability: state tags, input lock, killer resolution, score notification
  • Respawn orchestration, spawn invincibility, team scoring, match-end check

Partial / planned:

  • UUZ_GameplayAbility_Reload: class exists, behavior not implemented
  • Predicted fire/reload for low-latency feel
  • Additional inventory/weapon modularization

What I'd Do Next

  1. Implement reload ability behavior and connect ammo attribute to cooldown gating
  2. Add client-side prediction for fire to reduce perceived latency on higher RTT sessions
  3. Profile GE application throughput at sustained automatic fire rates under 16ms frame budget
  4. Integrate inventory modularization for multi-weapon support
  5. Extend HUD to show weapon state, reload phase, and team score progression