Skip to content
Back to wiki

Bullet System

A

Author

Author

Created: Apr 15, 2025Updated: Oct 20, 2026

Table of contents

Overview

The Bullet System is part of the GCS, designed to support ranged combat, spells, and projectiles.

As a heavy player of Elden Ring, I want my bullet system to be as excellent as that of Elden Ring. To achieve this, I have studied extensively how spells work in Elden Ring and implemented similar mechanics in Unreal Engine. You can see how impressive Elden Ring's bullet system is, and my version will continue to improve: Elden Ring Bullet System.

In GCS, you can quickly add various types of bullets to achieve different game mechanics.

Bullet Definition

In real projects, you might define thousands of bullets. Therefore, bullets are managed through a DataTable, and all object references use soft object references, so you don’t need to worry about my code causing your project to explode.

You manage and edit all bullets in your project by creating a DataTable asset with the structure type GCS_BulletDefinition.

Each row represents a bullet definition, and within each definition, you can configure many parameters. The most important parameters are as follows (other parameters are commented and will not be elaborated on here):

  • BulletActorClass: Specifies the bullet ActorClass corresponding to this bullet definition. You can choose from the defaults or define a special bullet instance for specific bullets.
  • BulletCount: This controls how many bullet instances will be generated when using this definition. Combined with the launch configuration (LaunchConfiguration), you can create many interesting bullet effects.
  • AttackDefinition: Use this option to select different attack definitions. Upon hitting a target, the bullet will apply game effects to the target based on the information specified in the attack definition.

Bullet Instances

All bullet instances are based on the C++ class AGCS_BulletInstance; the sample content includes the Blueprint subclass BP_GCS_Projectile. The component reuses instances through an object pool, so put per-activation and per-return logic in the Bullet lifecycle callbacks instead of relying on Actor BeginPlay / EndPlay to run for every use.

The default implementation is general enough that you typically won’t need to create new Blueprint subclasses.

Triggering Bullets via Animation

See Combat Attack/Defense for the current animation notify, melee AbilityTask, and bullet-request flow.

Triggering Bullets via Blueprints

The current runtime entry point is UGCS_BulletSystemComponent. Submit an UGCS_AttackRequest_Bullet through RequestSpawnBullet; the component creates and manages bullet batches and instances, and replicated batch entries use a Fast Array. UGCS_BulletSubsystem remains as a legacy compatibility layer and its public APIs are deprecated. Migrate existing Blueprint calls to the BulletSystemComponent on the owning Actor.

Bullet instances carry effects through the GCS_EffectCauser interface. The BulletSystemComponent builds a target GameplayEffect or EffectContainer from the bullet definition's AttackDefinition, and writes the AttackDefinition handle and BulletInstance into the EffectContext. Only the server-authoritative hit path applies those effects; the attribute callback then enters CombatFlow to produce the attack result. Do not replicate attack results directly from the Bullet layer.

The AttackRequest is a default instantiated object. Therefore, when you create a variable of type AttackRequest in any Blueprint, you create an instance of AttackRequest, not referencing an already existing AttackRequest object. The instantiated request will be saved with the Blueprint it resides in.

Bullet Object Pool

All created bullet instances are managed through an object pool. In simple terms, once a bullet completes its purpose, it will be disabled and cached. When a new bullet needs to be created, the system will first look for an available bullet from the cached ones and reactivate it. This significantly alleviates the performance costs associated with frequently creating and destroying bullet actors.

Therefore, when customizing your bullet actor, you need to be aware that:

  • The BeginPlay and EndPlay of the bullet instance are usually called only once, but OnBulletBeginPlay and OnBulletEndPlay can be called repeatedly.

Bullet Client Prediction

Prediction is limited to the first batch of a player-initiated bullet request and covers trajectory and visual effects only; Blueprint implements the local presentation. The component confirms or denies the prediction against the replicated server batch using its EntryId. Predicted bullets do not apply GameplayEffects or perform authoritative hit settlement. Hits and effects are always settled on the server, and hit-triggered chain and emitter bullets are server-only.

Runtime State Layers and Follow-up Bullets

Three state layers

Layer

Job

Network

FGCS_BulletEntry

One fire batch: definition row, source request, parent batch, predicted flag

FastArray replication

FGCS_BulletState

One instance: pose, velocity, distance, Trace handles, explosion deadline

Local runtime

FGCS_BulletHitState

Hits, penetrations, and cooldown against one actor

Local history

The public entry is RequestSpawnBullet / RequestSpawnBulletWithEntryId. After validating UGCS_AttackRequest_Bullet, a locally controlled client without authority predicts: spawn locally with TrySpawnBulletEntry, then call Server_SpawnBullet. Authority and AI spawn directly. When IsAIAttack is true (the source is not a locally controlled player), local prediction is skipped.

Authoritative hits call ApplyGameplayEffects, which forwards to the instance's Effect Causer. Without gameplay authority the path returns immediately, so a predicted bullet that touches a pawn does not change attributes.

Prediction confirmation and cleanup

The component header states the contract: predict only the first player-initiated batch's trajectory and visuals; predicted bullets must not apply GE; chain and emitter bullets are server-only. The client is confirmed or denied through Client_PredictedBulletResult. PredictionTimeout cleans stale batches. A late-replicated batch fast-forwards its instances by recorded age to hide replication delay.

Penetration, chain, and emitter

Definition flags decide whether a hit continues: bPenetrateCharacter / MaxPenetrationCount for pawns, bPenetrateMap for geometry. bUseSharedHitList shares hit history across instances in the same Entry so a spread cannot hit the same pawn twice. DamageHitDuration is the per-pawn hit cooldown.

If HitBulletDefinition is set, authority calls RequestSpawnBulletChain for a follow-up batch. Launch conditions read the native tag namespace GCS_BulletLaunch (Always / DidNotHitPawn / HitPawn). Spawn pose reads GCS_BulletChain (FromHitResult, FromSourceRequest, HitLocWithOwnerFwdDir, and related tags).

A bullet with EmitterBulletDefinition calls EmitBullet on an interval. Source type reads GCS_BulletEmitter (Default reuses the parent transform, ShooterWithRandomRadius, TargetingSystem). Public chain and emitter functions are BlueprintAuthorityOnly.

Instance contract

Pooled actors must implement IGCS_BulletInterface. The system assigns BulletId / EntryId / definition handle, then cycles OnBulletBeginPlay → OnBulletLaunch → OnBulletHit → OnBulletEndPlay → OnBulletReset. AGCS_BulletInstance is the abstract actor with projectile movement and Niagara/Cascade; GameplayEffects are still applied by the system on authority. AGCS_SphereBulletInstance adds a sphere collision and is the usual Blueprint-derivable default.

TraceDefinitions on the row register on the owner's Trace component at launch and override the instance class defaults. The instance creates ProjectileFX / ImpactFX on activate and on settled hits.

A concrete scenario

The player fires on the client. RequestSpawnBullet immediately takes an AGCS_SphereBulletInstance from the pool so the trail is visible. The server confirms the same EntryId; the authoritative shot keeps flying, and the predicted add is skipped on replication. After the first enemy, the definition allows one pawn penetration, HitState records it, and authority applies the attack definition's GE. The second hit is a wall; chain tag FromHitResult spawns an explosion bullet at the impact. An AI shooter fires on the server, so the whole path skips prediction.