Skip to content

Core Documentation

opsidiumdesigns edited this page Jun 12, 2026 · 1 revision

Bullet Forge

Version: 1.7.0

Bullet Forge is a high-performance, networked ballistics plugin for Unreal Engine 5. It provides a robust framework for realistic bullet physics, including trajectory simulation, material penetration, ricochets, and flyby audio/visual events.

Table of Contents

Quick Start / Integration Guide

  1. Setup Weapon: Add UBulletForgeWeaponComponent to your weapon actor.
  2. Configure Ammo: Create a UBallisticsSettings Data Asset for your ammunition.
  3. Fire: For single/manual shots, call FireBullet on the weapon component from the owning client or authoritative server, passing in the muzzle transform and your ballistics settings. FireBullet handles prediction, seed generation, batching, and server RPCs. For semi-auto/full-auto/burst fire loops, prefer the cadence engine (StartCadenceFire, StartCadenceFireWithProvider, or TryFireCadenceShot). Use ServerFireBullet only for advanced/manual integrations and provide a nonzero TraceSeed.
  4. Setup Targets: For actors that need to react to bullets, add a UBulletForgeReactiveComponent and bind to its events. Implement IBulletForgeInterface if you need fine-grained control over how the bullet interacts with that specific actor.

What Changed in 1.7.0

Bullet Forge 1.7.0 focuses on making automatic fire loops less sensitive to local prediction cost and Blueprint timer drift:

  • Full client prediction now defaults to Deferred execution through project settings.
  • UBulletForgeWeaponComponent::PredictionExecutionMode defaults to ProjectDefault; if the project setting is missing or invalid, Bullet Forge falls back to Deferred.
  • Client FireBullet queues/sends the server fire request before local prediction work, so same-frame prediction does not delay the caller's next scheduling decision.
  • Deferred local prediction is processed during component tick and capped by MaxDeferredPredictedShotsPerTick per weapon component.
  • The cadence engine adds optional semi-auto, full-auto, and burst helpers for stable weapon loops.
  • Weapon actors can implement IBulletForgeCadenceProvider to supply cadence can-fire checks, per-shot requests, and shot/blocked callbacks without subclassing the Bullet Forge component.
  • Delayed-shot diagnostics can be enabled for cadence, batching, RPC, or server validation timing captures.
  • Trace pressure diagnostics can be enabled for active trace lifetime/die-off, cosmetic playback pressure, visual follower backlog, visual component churn, FX scheduling, or fire-batch size distribution captures.
  • Trace pressure and tick diagnostics now expose retained passive counters for line queries, trace segments, hit handling, penetration backtraces, ricochets, and flyby overlap timing/counts.
  • Diagnostics remain gated and off by default; they are instrumentation-only.
  • Owner-local stale impact visual/audio multicasts now share the bounded recent seed suppression semantics used by stale owner muzzle/shell cosmetics, preventing late local replays without changing remote-client impact FX or gameplay traces.
  • Simulated-proxy clients now suppress only stale impact visual/audio multicasts whose replay lateness exceeds the existing fire cosmetic late threshold, leaving valid remote impact FX and all gameplay trace behavior unchanged.

Key Features

  • Physically-Based Ballistics: Simulates bullet drop, drag, and energy loss over distance using configurable ballistic coefficients and air density.
  • Advanced Penetration System: Calculates material penetration based on density, thickness, and hardness.
  • Dynamic Ricochets: Realistic ricochet logic based on incidence angles and material properties.
  • Networked Prediction: Built to work in multiplayer environments with server-side validation and configurable client prediction. Full prediction can run local traces, Partial Forward predicts only lightweight cosmetics/impact feedback, and None relies on server-driven cosmetics. The server's ActiveTraces are not replicated to avoid jitter.
  • Flyby System: Detects near-misses (flybys) and triggers events for audio/visual feedback.
  • Blueprint Function Library: Helper functions for physics-based damage calculations.
  • Reactive System: Actors can respond to hits, penetrations, and ricochets via specialized components and interfaces.

Technical Details

  • Language: C++
  • Dependencies: Niagara, Physical Materials.
  • Network: Supports Client/Server architecture.

Core Components

UBulletForgeWeaponComponent

The heart of the system. This component should be attached to weapons or pawns to handle firing and tracing of bullets. It manages the lifecycle of each "bullet trace" and handles all physics calculations.

  • Fire Interval Validation: Set FireIntervalSeconds or override GetFireIntervalSeconds(Settings) for Bullet Forge's low-level validation interval. It is used by FireBullet and cadence clamping, but it is not a full weapon/attachment rate system.
  • Prediction Mode: Set PredictionMode or override GetPredictionMode to control client-side prediction (None, Partial Forward, Full).
  • Prediction Execution: Set PredictionExecutionMode to choose whether client prediction runs immediately in FireBullet or is deferred to Bullet Forge tick. Deferred is the default for responsiveness.
  • Cadence Engine: Use StartCadenceFire, StartCadenceFireWithProvider, StopCadenceFire, and TryFireCadenceShot for semi-auto, full-auto, and burst fire loops that are not coupled to one shot's simulation cost.

IBulletForgeCadenceProvider

Weapon actors can implement this interface to provide dynamic cadence behavior while keeping UBulletForgeWeaponComponent generic. This is the recommended path for automatic or burst weapons that own ammo, reload state, fire mode, spread, muzzle transforms, animations, or gameplay effects on the weapon actor.

StartCadenceFireWithProvider uses the owning actor as the dynamic request/can-fire/callback path. Implement IBulletForgeCadenceProvider on the weapon actor that owns the component; Bullet Forge discovers the owner interface automatically.

Provider functions receive the UBulletForgeWeaponComponent* so one actor can support multiple Bullet Forge components. Provider cadence requires the owning actor provider to implement CanBulletForgeCadenceFire for weapon-owned checks such as ammo and reload state, and BuildBulletForgeFireRequest for fresh per-shot request data. BuildBulletForgeFireRequest must return true after it fills a valid request; returning false tells Bullet Forge the request failed and blocks the shot with BuildRequestFailed. HandleBulletForgeCadenceShotFired and HandleBulletForgeCadenceFireBlocked are optional callbacks for accepted-shot and blocked-shot feedback. If the owner does not implement the provider interface, provider cadence reports NoCadenceProvider and stops.

UBulletForgeRewindComponent

Attach this to actors that should be considered for lag compensation. Register one or more rewind targets (capsule, mesh hitboxes, or custom collision proxies) so BulletForge can rewind those transforms during server-side hit validation. Auto-registration is enabled by default for capsules and skeletal meshes.

UBulletForgeReactiveComponent

Attach this component to actors that should respond to ballistics. It provides several multicast delegates:

  • OnHitEvent: Triggered on any impact. Provides CurrentEnergy and EnergyLoss (the energy lost during the impact).
  • OnPenetrationEvent: Triggered when a bullet passes through the actor.
  • OnRicochetEvent: Triggered when a bullet bounces off the actor.
  • OnFlybyEvent: Triggered when a bullet passes near the actor.

IBulletForgeInterface

An interface that can be implemented by Actors or Components to provide custom data to the ballistics system, such as:

  • Custom penetration resistance.
  • Custom ricochet thresholds.
  • Overriding material thickness or hardness.

Reactive component lookup first asks IBulletForgeInterface::GetReactiveComponent when the hit actor implements the interface. If that function returns nullptr or is left at the default implementation, Bullet Forge falls back to automatically finding a UBulletForgeReactiveComponent on the actor with FindComponentByClass. Override GetReactiveComponent only when you need to return a specific/non-standard reactive component.

UBulletForgeFunctionLibrary (Static Helpers)

A library of static functions accessible from both C++ and Blueprints:

  • CalculateDamageFromHitEvent: The recommended way to scale damage based on impact energy.
  • CalculateDamageFromTrace: Scales damage using the internal trace state.

Configuration

UBallisticsSettings (Data Asset)

Defines the properties of a specific projectile or ammo type:

  • Muzzle Velocity: Initial speed in cm/s.
  • Mass: Bullet mass in grams.
  • Ballistic Coefficient: Drag model (G1 model).
  • Penetration/Ricochet Factors: Multipliers for energy loss and probability.
  • Visuals: Tracer Niagara and bullet mesh settings for cosmetic path playback.

Fire Interval Tuning

FireIntervalSeconds is a weapon-component rate-of-fire limiter. GetFireIntervalSeconds(Settings) is Bullet Forge's low-level validation hook and defaults to the component FireIntervalSeconds; Settings is optional ammo context, not a complete weapon attachment or fire-mode system. It gates both client prediction and server validation, so it should match the interval your weapon system currently allows. Override GetFireIntervalSeconds or update the component value when fire mode, attachments, buffs, or other weapon state changes.

  • RPM to interval: FireIntervalSeconds = 60.0 / RPM (or use ComputeFireIntervalFromRPM)
  • Example values:
    • 600 RPM → 0.100 s
    • 750 RPM → 0.080 s
    • 900 RPM → 0.067 s
    • 1200 RPM → 0.050 s
  • Burst weapons: Keep FireIntervalSeconds at the per-shot interval and control burst size in your weapon logic.
  • Weapon-owned rates: If your weapon actor or Blueprint already owns the current fire-rate value, push that value into the Bullet Forge component's FireIntervalSeconds whenever it changes.
  • Component overrides: If you want Bullet Forge to pull the value dynamically, subclass UBulletForgeWeaponComponent and override GetFireIntervalSeconds in C++ or Blueprint.

C++ weapon-owned value example:

const float CurrentFireInterval = UBulletForgeWeaponComponent::ComputeFireIntervalFromRPM(CurrentRPM);
BulletForgeWeaponComponent->FireIntervalSeconds = CurrentFireInterval;

C++ component override example:

float UMyBulletForgeWeaponComponent::GetFireIntervalSeconds_Implementation(const UBallisticsSettings* Settings) const
{
    const AMyWeaponActor* Weapon = Cast<AMyWeaponActor>(GetOwner());
    return Weapon ? Weapon->GetCurrentFireIntervalSeconds() : Super::GetFireIntervalSeconds_Implementation(Settings);
}

Blueprint example:

  1. Simple path: set the Bullet Forge component's FireIntervalSeconds from your weapon Blueprint's current fire-rate value.
  2. Dynamic path: create a Blueprint subclass of UBulletForgeWeaponComponent, override GetFireIntervalSeconds, and have it read the owner weapon's current interval.
  • Network sync: Use the same interval on client and server to avoid rejection due to ROF validation.
  • Batching tip: Set FireBatchInterval to be less than or equal to FireIntervalSeconds to keep added batching delay bounded; lower values reduce latency at higher RPC cost.

Cadence Engine

Bullet Forge 1.7 adds an optional cadence engine directly to UBulletForgeWeaponComponent. FireBullet remains the low-level "fire this shot now" API, but automatic and burst weapons should prefer the cadence helpers so fire timing is independent from prediction, tracing, Blueprint cosmetics, or asset compilation cost.

FBulletForgeCadenceSettings includes FireMode, FireIntervalSeconds, BurstCount, bAllowCatchUp, and MaxCatchUpShots. FireIntervalSeconds is the cadence engine's desired interval. Bullet Forge automatically clamps cadence scheduling to at least GetFireIntervalSeconds(Settings) for the request being fired, so a cadence setting that is accidentally faster than low-level validation will not churn rejected shots. If your project owns its fire-rate value elsewhere (for example, a weapon FireRateDelay), set both the component FireIntervalSeconds and cadence FireIntervalSeconds from the same source, or override GetFireIntervalSeconds to return the current validated interval.

Direct FireBullet is still valid for manual or one-shot integrations. The cadence APIs are optional, but they are recommended for automatic and burst loops because Bullet Forge owns timing, catch-up behavior, and blocked-shot reporting.

Supported modes:

  • Semi Auto: fires one shot and stops.
  • Full Auto: fires at cadence FireIntervalSeconds until StopCadenceFire is called, clamped to low-level validation.
  • Burst: fires BurstCount shots at cadence FireIntervalSeconds, clamped to low-level validation, then stops.

Usage styles:

  1. Stored request — best for simple weapons where shot data is stable while held:

    FBulletForgeFireRequest Request;
    Request.MuzzleTransform = MuzzleTransform;
    Request.Settings = BallisticsSettings;
    Request.OwningActor = OwningActor;
    
    FBulletForgeCadenceSettings Cadence;
    Cadence.FireMode = EBulletForgeFireMode::FullAuto;
    Cadence.FireIntervalSeconds = UBulletForgeWeaponComponent::ComputeFireIntervalFromRPM(600.0f);
    
    BulletForgeWeapon->StartCadenceFire(Request, Cadence);
    // On trigger release:
    BulletForgeWeapon->StopCadenceFire();
  2. Weapon actor provider — best for dynamic weapons where every shot needs a fresh muzzle transform, ammo asset, ignored actors, spread seed, owner, ammo check, reload check, or fire-mode state. Implement IBulletForgeCadenceProvider on the weapon actor that owns the component, then call StartCadenceFireWithProvider. Bullet Forge automatically uses the owner actor interface. Bind OnCadenceShotFired for accepted shots and OnCadenceFireBlocked for blocked shots (NoCadenceProvider, NullSettings, FireBulletRejected, BuildRequestFailed, or provider can-fire reasons).

  3. Manual helper — best when keeping an existing weapon loop. Call TryFireCadenceShot(Request, Cadence) instead of manually comparing timestamps; Bullet Forge applies the interval and advances cadence safely.

The component cadence API stays generic: stored-request cadence (StartCadenceFire), provider cadence (StartCadenceFireWithProvider), manual cadence (TryFireCadenceShot), stop/status helpers, and the OnCadenceShotFired / OnCadenceFireBlocked delegates. Weapon-specific request building, can-fire checks, and accepted/blocked callbacks belong on the owning actor's IBulletForgeCadenceProvider implementation.

Using IBulletForgeCadenceProvider

Use IBulletForgeCadenceProvider when the weapon actor owns state that changes per shot: ammo count, reload state, equipped state, fire mode, spread, muzzle sockets, ignored actors, or cosmetic/gameplay callbacks.

Setup flow:

  1. Implement IBulletForgeCadenceProvider on the weapon actor that owns the UBulletForgeWeaponComponent.
  2. On trigger press, create FBulletForgeCadenceSettings and call StartCadenceFireWithProvider(Settings).
  3. On trigger release, call StopCadenceFire().

Provider resolution checks the component owner only. If the owner does not implement IBulletForgeCadenceProvider, provider cadence blocks with NoCadenceProvider, logs BulletForge cadence blocked, broadcasts OnCadenceFireBlocked, and stops.

Required provider events/functions:

Function Use it for
CanBulletForgeCadenceFire Validate weapon-owned state before each shot, such as ammo, reload, equipped, safety, or heat state. Return true to allow firing. Return false and set OutBlockedReason to a useful FName when blocked; if this returns false without a reason, Bullet Forge reports CanFireBlocked.
BuildBulletForgeFireRequest Fill the per-shot FBulletForgeFireRequest: muzzle transform, ballistics settings, owning actor, ignored actors, debug values, and optional deterministic TraceSeed. Return true after filling a valid request. If this returns false, Bullet Forge reports BuildRequestFailed and does not shoot. Return false only when required data is missing. Keep this side-effect-light; consume ammo and start gameplay feedback only after a shot is accepted.

Blueprint providers should implement both required events explicitly. Do not rely on interface fallback behavior for provider cadence; native fallbacks fail closed so missing required implementations block firing instead of silently allowing incomplete shots.

Optional callbacks:

Function Use it for
HandleBulletForgeCadenceShotFired Run accepted-shot logic after Bullet Forge accepts the request, such as consuming ammo, starting recoil, playing animations, spawning weapon effects, or notifying gameplay code.
HandleBulletForgeCadenceFireBlocked React to blocked cadence attempts. Common reasons include NoCadenceProvider, CanFireNotImplemented, CanFireBlocked, BuildRequestFailed, NullSettings, FireBulletRejected, plus any custom reason returned by CanBulletForgeCadenceFire.

Expected trigger flow:

  1. StartCadenceFireWithProvider starts the cadence loop from the trigger press.
  2. For each due shot, Bullet Forge calls BuildBulletForgeFireRequest so it can get current shot data and resolve the effective cadence interval from the request settings.
  3. Bullet Forge calls CanBulletForgeCadenceFire before firing the built request.
  4. If the request is valid, can-fire passes, and FireBullet accepts it, Bullet Forge calls HandleBulletForgeCadenceShotFired and broadcasts OnCadenceShotFired.
  5. If any step blocks, Bullet Forge calls HandleBulletForgeCadenceFireBlocked and broadcasts OnCadenceFireBlocked with the reason.
  6. StopCadenceFire ends the loop on trigger release or when your weapon logic cancels firing.

Compact C++ example:

// Header
UCLASS()
class AMyWeaponActor : public AActor, public IBulletForgeCadenceProvider
{
    GENERATED_BODY()

public:
    void PullTrigger();
    void ReleaseTrigger();

    virtual bool CanBulletForgeCadenceFire_Implementation(UBulletForgeWeaponComponent* WeaponComponent, FName& OutBlockedReason) override;
    virtual bool BuildBulletForgeFireRequest_Implementation(UBulletForgeWeaponComponent* WeaponComponent, FBulletForgeFireRequest& OutRequest) override;
    virtual void HandleBulletForgeCadenceShotFired_Implementation(UBulletForgeWeaponComponent* WeaponComponent, const FBulletForgeFireRequest& Request) override;
    virtual void HandleBulletForgeCadenceFireBlocked_Implementation(UBulletForgeWeaponComponent* WeaponComponent, FName Reason) override;

private:
    UPROPERTY(VisibleAnywhere)
    TObjectPtr<UBulletForgeWeaponComponent> BulletForgeWeapon;

    UPROPERTY(EditDefaultsOnly)
    TObjectPtr<UBallisticsSettings> AmmoSettings;
};

// Source
void AMyWeaponActor::PullTrigger()
{
    FBulletForgeCadenceSettings Cadence;
    Cadence.FireMode = EBulletForgeFireMode::FullAuto;
    Cadence.FireIntervalSeconds = UBulletForgeWeaponComponent::ComputeFireIntervalFromRPM(750.0f);
    Cadence.bAllowCatchUp = false;

    BulletForgeWeapon->StartCadenceFireWithProvider(Cadence);
}

void AMyWeaponActor::ReleaseTrigger()
{
    BulletForgeWeapon->StopCadenceFire();
}

bool AMyWeaponActor::CanBulletForgeCadenceFire_Implementation(UBulletForgeWeaponComponent* WeaponComponent, FName& OutBlockedReason)
{
    if (!AmmoSettings)
    {
        OutBlockedReason = FName(TEXT("NoAmmoSettings"));
        return false;
    }

    if (IsReloading() || GetAmmoInMagazine() <= 0)
    {
        OutBlockedReason = FName(TEXT("NoAmmo"));
        return false;
    }

    OutBlockedReason = NAME_None;
    return true;
}

bool AMyWeaponActor::BuildBulletForgeFireRequest_Implementation(UBulletForgeWeaponComponent* WeaponComponent, FBulletForgeFireRequest& OutRequest)
{
    if (!WeaponComponent || !AmmoSettings)
    {
        return false;
    }

    OutRequest.MuzzleTransform = GetMuzzleTransformWithSpread();
    OutRequest.Settings = AmmoSettings;
    OutRequest.OwningActor = GetOwner() ? GetOwner() : this;
    OutRequest.IgnoredActors.Add(this);
    OutRequest.TraceSeed = MakeShotTraceSeed(); // Optional; leave zero to let Bullet Forge generate one.
    return true;
}

void AMyWeaponActor::HandleBulletForgeCadenceShotFired_Implementation(UBulletForgeWeaponComponent* WeaponComponent, const FBulletForgeFireRequest& Request)
{
    ConsumeAmmo(1);
    PlayRecoilAndMuzzleEffects(Request.MuzzleTransform);
}

void AMyWeaponActor::HandleBulletForgeCadenceFireBlocked_Implementation(UBulletForgeWeaponComponent* WeaponComponent, FName Reason)
{
    if (Reason == FName(TEXT("NoAmmo")))
    {
        StartReloadOrDryFireFeedback();
    }
}

Blueprint users can implement the same interface on the weapon Blueprint that owns the Bullet Forge component. Add the Bullet Forge cadence provider interface, implement the can-fire/build/shot-fired/blocked events, fill the FBulletForgeFireRequest output pins in Build Bullet Forge Fire Request, make sure the event returns true after filling the request, call Start Cadence Fire With Provider on trigger press, and call Stop Cadence Fire on trigger release.

Troubleshooting provider cadence:

  • If provider cadence does not shoot, check the log for BulletForge cadence blocked.
  • Common reasons include NoCadenceProvider, BuildRequestFailed, CanFireNotImplemented, CanFireBlocked, NullSettings, and FireBulletRejected.
  • BuildRequestFailed usually means BuildBulletForgeFireRequest returned false; verify the request is filled and the function returns true when valid.

Catch-up behavior:

  • bAllowCatchUp = false skips missed shots after a hitch and resumes cadence forward. This is the safest default for competitive/full-auto weapons.
  • bAllowCatchUp = true allows bounded recovery after a hitch.
  • MaxCatchUpShots caps the number of immediate recovery shots so a stalled frame cannot dump an unbounded burst.

Migration note for Blueprint full-auto weapons:

  • Avoid recursive one-shot timer patterns like Fire -> FireBullet -> SetTimerDelegate(FireRateDelay, false).
  • That pattern schedules the next shot after FireBullet returns, so any synchronous prediction/effect work can delay cadence.
  • Prefer implementing IBulletForgeCadenceProvider on the weapon actor, then call StartCadenceFireWithProvider on trigger press and StopCadenceFire on trigger release. Use TryFireCadenceShot if you need to keep an existing pump.

Firing and Cadence Network Context

Bullet Forge separates who schedules a shot from who owns gameplay results. Player weapons should usually start firing on the owning client so input, muzzle flash, shell ejection, and predicted impact feedback are responsive. Server-owned weapons, AI weapons, and non-player systems should fire from the authoritative server. In all cases, server simulation owns authoritative hit validation, penetration, ricochet, damage, and server-side gameplay decisions.

Action/Event Runs on Who calls it Notes
FireBullet for a player weapon Owning client Your input/weapon code Locally rate-limits, builds a pending request, sends or enqueues ServerFireBullet / ServerFireBulletBatch first, then runs or queues local prediction when prediction mode allows it.
FireBullet for AI or server-owned weapons Authoritative server Server gameplay/AI code Processes authoritative bullet simulation directly and broadcasts cosmetic effects to clients.
ServerFireBullet / ServerFireBulletBatch Server Bullet Forge normally calls these from client FireBullet; advanced integrations may call them manually Validates request shape, trace seed, and fire interval before authoritative simulation. Gameplay hit decisions remain server-owned.
StartCadenceFire, StartCadenceFireWithProvider, TryFireCadenceShot The machine where you call them, then that component's tick Your weapon/input/AI code Local scheduling helpers around FireBullet. If called on the owning client, cadence runs on the client. If called on the server, cadence runs on the server.
IBulletForgeCadenceProvider::BuildBulletForgeFireRequest Same side running the cadence loop Bullet Forge cadence Fills the local FireBullet request: muzzle transform, UBallisticsSettings, owning actor, ignored actors, debug fields, and optional trace seed. For client-started firing, the request data used by the RPC must be valid and available on the client.
IBulletForgeCadenceProvider::CanBulletForgeCadenceFire Same side running the cadence loop Bullet Forge cadence Checks local weapon state before each shot. For client-started firing, mirror server-authoritative state where practical so local prediction does not frequently disagree with server validation.
HandleBulletForgeCadenceShotFired / OnCadenceShotFired Same side running the cadence loop Bullet Forge after local FireBullet accepts the shot Good for local ammo consumption, recoil, animation, and weapon feedback. On a client, this is not proof that the server confirmed a hit or damage.
HandleBulletForgeCadenceFireBlocked / OnCadenceFireBlocked Same side running the cadence loop Bullet Forge when local cadence or local FireBullet blocks Reports local cadence blocks such as missing provider, can-fire failure, build failure, null settings, or local fire interval rejection. It is not a general server rejection event.
Reactive hit/penetration/ricochet/flyby events and server gameplay handling Server for authoritative gameplay; clients for replicated/cosmetic reactions Bullet Forge trace processing and reactive components Use these paths for hit, damage, armor, and gameplay reactions. Do not use client cadence callbacks as authoritative damage confirmation.

Prediction execution only changes when local client work happens. With PredictionExecutionMode::Immediate, allowed client prediction runs inside the FireBullet call after the server request path is sent or enqueued. With PredictionExecutionMode::Deferred, Bullet Forge queues the prediction work and processes it from component tick, capped by MaxDeferredPredictedShotsPerTick. Both modes still send/enqueue the server request before local prediction, and both rely on the server for authoritative gameplay outcomes.

For networked weapons:

  • Do call FireBullet or start cadence from the owning client for player weapons that need responsive local feedback.
  • Do call FireBullet or start cadence from the authoritative server for server-owned or AI weapons.
  • Do keep muzzle transforms, ballistics settings, owning actor references, ignored actors, fire interval state, and optional trace seed inputs available on the side that starts cadence.
  • Do keep client can-fire checks close to server-authoritative state for ammo, reload, heat, fire mode, and fire interval decisions when client-started prediction is enabled.
  • Do not start the same weapon's cadence loop on both client and server at the same time; choose one scheduler for that weapon.
  • Do not treat client provider callbacks, cadence delegates, predicted impact effects, muzzle effects, shell effects, or tracer visuals as authoritative damage confirmation.
  • Do not put required server-only shot data exclusively on the server if the owning client starts cadence; the client must be able to build the request that Bullet Forge sends to the server.

Tracer Niagara and Bullet Mesh Visuals

Bullet Forge uses a per-trace cosmetic visual path follower for tracer Niagara and bullet mesh playback. The visual follower replays ordered path samples from the simulated bullet path, including drop and ricochets, while the authoritative Bullet Forge traces continue to drive hit validation, penetration, ricochet, and damage events.

These visuals are cosmetic only. They do not replace Bullet Forge traces, and enabling mesh collision on a visual bullet mesh does not make that mesh authoritative for Bullet Forge damage.

Shell ejection effects are weapon/Blueprint-owned rather than ammo-owned. Bind or implement your custom weapon's shell logic from SpawnShellEffects so casing meshes, sockets, pooling, and weapon-specific Niagara systems stay with the weapon Blueprint.

Niagara tracer setup
  1. Create or choose a Niagara System for the tracer. This section documents the setup; it does not create any assets.

  2. Choose one of these visual styles:

    • Ribbon/trail attached to the moving visual proxy: Use the proxy motion to draw the trail over time. This is best when you want the tracer to visibly follow bullet drop or ricochet samples.
    • Beam-style system: Read Bullet Forge user parameters and render a beam between the current path sample endpoints.
  3. For beam-style systems, add these Niagara user parameters with the default Bullet Forge names and types:

    Niagara parameter Type Default setting that supplies the name
    User.TraceStart Vector TracerStartParameterName = TraceStart
    User.TraceEnd Vector TracerEndParameterName = TraceEnd
    User.TraceDirection Vector TracerDirectionParameterName = TraceDirection
    User.TraceLength Float TracerLengthParameterName = TraceLength
    User.TraceDuration Float TracerDurationParameterName = TraceDuration
  4. Configure the Niagara renderer or modules:

    • Ribbon/trail mode: Attach the ribbon/trail to the moving visual proxy and let the proxy motion generate the trail. Use Niagara lifetime and ribbon settings to control persistence.
    • Beam mode: Bind beam start/end, direction, length, and duration logic to the User.* parameters above.
  5. Assign the Niagara System to UBallisticsSettings::TracerEffect on the ammo settings data asset.

  6. Tune TracerVisualRotationOffset if the tracer's authored forward axis does not match Bullet Forge's visual travel direction.

  7. Tune lifetime with TracerVisualMinLifetime and TracerVisualMaxLifetime. Keep the range short for fast rounds and increase it only when the visual needs more persistence.

Tracer settings on UBallisticsSettings:

  • TracerEffect
  • TracerVisualRotationOffset
  • TracerStartParameterName (default TraceStart)
  • TracerEndParameterName (default TraceEnd)
  • TracerDirectionParameterName (default TraceDirection)
  • TracerLengthParameterName (default TraceLength)
  • TracerDurationParameterName (default TraceDuration)
  • TracerVisualMinLifetime
  • TracerVisualMaxLifetime
Bullet mesh setup
  1. Assign a static mesh to UBallisticsSettings::BulletMesh.
  2. Set BulletMeshVisualRotationOffset so the mesh points along the visual path follower direction. Use this when the mesh's local forward axis differs from Bullet Forge's travel direction.
  3. Set BulletMeshVisualScale for the spawned mesh size.
  4. Set BulletMeshVisualMinLifetime and BulletMeshVisualMaxLifetime to control how long the mesh proxy remains visible while following the sampled path.
  5. Leave collision disabled unless your visual mesh needs cosmetic overlap or hit events:
    • Default collision is BulletMeshVisualCollisionEnabled = NoCollision.
    • Default preset is BulletMeshVisualCollisionProfileName = NoCollision.
    • Default overlap generation is bBulletMeshVisualGenerateOverlapEvents = false.
  6. To opt in to cosmetic mesh collision, configure BulletMeshVisualCollisionProfileName with the engine collision preset picker (FCollisionProfileName), then tune BulletMeshVisualCollisionEnabled and bBulletMeshVisualGenerateOverlapEvents like the Collision section of a default Static Mesh Component in a Blueprint. If the preset is Custom, BulletMeshVisualObjectType and BulletMeshVisualCollisionResponses are also applied. This collision is still visual/cosmetic and does not replace Bullet Forge hit validation or damage.

Bullet mesh settings on UBallisticsSettings:

  • BulletMesh
  • BulletMeshVisualRotationOffset
  • BulletMeshVisualScale
  • BulletMeshVisualMinLifetime
  • BulletMeshVisualMaxLifetime
  • BulletMeshVisualCollisionEnabled (default NoCollision)
  • BulletMeshVisualCollisionProfileName (FCollisionProfileName, default NoCollision)
  • bBulletMeshVisualGenerateOverlapEvents (default false)
  • BulletMeshVisualObjectType
  • BulletMeshVisualCollisionResponses
Troubleshooting visuals
Visual result Check
Tracer is sideways or backwards Adjust TracerVisualRotationOffset. Confirm the Niagara effect's authored forward axis matches the moving proxy or beam direction.
Tracer appears segmented or disconnected Prefer ribbon/trail mode for continuous motion, increase Niagara trail persistence slightly, or verify the beam renderer is using User.TraceStart and User.TraceEnd each update.
Tracer does not follow drop or ricochet Use the moving visual proxy/ribbon path. For beam-style tracers, make sure modules consume the updated User.TraceStart, User.TraceEnd, User.TraceDirection, User.TraceLength, and User.TraceDuration values instead of fixed emitter values.
Niagara parameters are not updating Confirm the Niagara parameter names include the User. namespace and match the UBallisticsSettings name fields. If you rename a setting value, rename the Niagara user parameter to match.
Bullet mesh is invisible Confirm BulletMesh is assigned, BulletMeshVisualScale is nonzero, lifetimes are long enough to see, and the mesh material is visible in the current lighting/post-process setup.
Bullet mesh collision is not firing Collision is off by default. Choose a non-NoCollision BulletMeshVisualCollisionProfileName preset, set BulletMeshVisualCollisionEnabled, enable bBulletMeshVisualGenerateOverlapEvents for overlap callbacks, and verify object type/responses.
Multiplayer clients do not see visuals Confirm the weapon is firing through Bullet Forge's normal predicted/server path, cosmetic multicasts are not culled by EffectCullDistance, and tracer batching/cosmetic suppression settings are not hiding the expected effect.
Validation checklist

Use PIE to validate the visual setup:

  1. Start PIE with 2+ players using the project's normal multiplayer test mode.
  2. Fire a weapon using a UBallisticsSettings asset with TracerEffect and/or BulletMesh assigned.
  3. Confirm the tracer and bullet mesh follow bullet drop over distance.
  4. Fire at a ricochet-capable surface and confirm visuals follow the post-ricochet path sample instead of drawing a straight muzzle-to-impact line.
  5. Confirm hit, penetration, ricochet, and damage behavior still comes from Bullet Forge traces/reactive events, not from visual mesh collision.
  6. On remote clients, confirm visuals appear or are intentionally culled by EffectCullDistance.

Bullet Forge Developer Settings

Global settings located in Project Settings -> Plugins -> Bullet Forge:

  • Air Density and Default Gravity.
  • Performance limit (MaxTracesPerTick) and gated diagnostics for profiling trace, flyby, and cosmetic pressure.
  • Fallback material properties (Default density, thickness).
  • Network prediction, batching, cosmetic suppression, fire-rate tolerance, effect culling, lag compensation, and dedicated diagnostics settings.

Performance Controls (Developer Settings)

MaxTracesPerTick remains the legacy active-trace count cap. Use the gated diagnostics below to measure trace and flyby cost without changing runtime behavior.

Networking Model (Deterministic Prediction)

Bullet Forge uses a server-authoritative model with deterministic client prediction and reconciliation. In Full prediction mode, clients run the same trace simulation locally for responsiveness while the server simulates the authoritative trace and sends corrections when needed. By default, Full prediction work is deferred to Bullet Forge tick so FireBullet returns quickly and weapon fire cadence is not coupled to local prediction cost. Set PredictionExecutionMode to Immediate if a legacy integration requires same-call-stack local prediction. In Partial Forward mode, clients predict local muzzle/shell cosmetics and a lightweight cosmetic impact probe while gameplay traces remain server-authored. In None mode, clients rely on server-driven cosmetics only. Visual effects are multicast while gameplay decisions remain server-owned.

  • Deterministic Seeds: Each fired trace is seeded on the client and sent to the server. Both sides use the same seed so spread and ricochet randomness match.
  • Trace Checkpoints: The server periodically sends lightweight checkpoints (position, direction, energy) to the owning client to reconcile prediction.
  • Lag Compensation: Server rewinds registered targets to the shot timestamp for hit validation (targets opt in via UBulletForgeRewindComponent).
  • Tracer Batching: Tracer effects are grouped into batches per tick to reduce RPC traffic and burst spam.
  • Fire Request Batching: Client fire requests are grouped into batches for full-auto weapons to reduce per-shot RPC volume.
  • Cosmetic RPCs: Impact, muzzle, shell, and tracer effects are cosmetic-only and sent via Unreliable multicast.
  • Client Effect Culling: Clients skip spawning effects beyond EffectCullDistance to avoid unnecessary rendering.
  • Predicted Impact Effects: Clients spawn impact FX immediately on hit and suppress the duplicate server multicast if it matches the predicted impact location.
  • Impact Time-of-Flight: Impact FX are scheduled using simulated ballistic travel time to preserve realistic arrival delays across clients.
  • Impact Audio Delay: Impact audio is scheduled via SpawnImpactAudioEffect using speed-of-sound delay on top of time-of-flight.
  • Flyby Timing: Flyby events are scheduled using ballistic travel time plus speed-of-sound delay, with pitch/volume values provided for audio tuning.
  • Server-Accurate Debug: Debug traces are drawn on the server to keep authoritative visuals accurate.

Networking Settings (Developer Settings)

  • bEnableDeterministicPrediction: Enables deterministic random streams so client/server spread and ricochet match.
  • DefaultPredictionExecutionMode: Project-wide prediction execution default. Deferred is the default and processes local prediction from Bullet Forge tick; Immediate preserves legacy same-call-stack prediction. If a component is set to ProjectDefault and this project setting is unavailable or set back to ProjectDefault, Bullet Forge resolves to Deferred.
  • MaxDeferredPredictedShotsPerTick: Per-component budget for deferred local predicted shots processed each tick. Raise only if prediction queues visibly fall behind; lower to smooth per-frame prediction cost.
  • bEnableTraceCheckpoints: Sends server checkpoints to reconcile client prediction.
  • TraceCheckpointInterval: Minimum time (seconds) between checkpoints per trace.
  • TraceCheckpointDistance: Minimum travel distance (cm) between checkpoints.
  • TraceCorrectionDistanceThreshold: Distance (cm) required before a correction is applied.
  • TraceCorrectionAngleThreshold: Angle (degrees) required before a correction is applied.
  • bEnableTracerBatching: Batches tracer effects to reduce RPC volume.
  • bEnableFireBatching: Batches fire requests to reduce per-shot RPC volume on automatic weapons.
  • TracerBatchInterval: Minimum time (seconds) between tracer batches.
  • FireBatchInterval: Minimum time (seconds) between fire request batches.
  • MaxTracerSegmentsPerBatch: Cap on tracer segments per batch RPC.
  • MaxFireRequestsPerBatch: Cap on fire requests per batch RPC.
  • EffectCullDistance: Maximum distance (cm) for spawning client-side effects (0 = always spawn).
  • bEnablePredictedCosmeticSuppression: Suppresses server muzzle/shell multicasts on clients that already predicted the same shot locally, using deterministic trace-seed matching when available and the unseeded fallback window otherwise.
  • PredictedCosmeticSuppressMaxAgeSeconds: How long a client remembers locally predicted muzzle/shell effects by trace seed before unmatched entries expire.
  • UnseededCosmeticSuppressWindowSeconds: Compatibility fallback window for legacy or custom cosmetic multicasts that do not include a trace seed (0 disables unseeded fallback suppression).
  • bEnableFireIntervalTolerance: Lets the fire-rate limiter accept shots that arrive slightly before the exact fire interval to smooth timer-driven full-auto cadence.
  • FireIntervalToleranceFraction: Fraction of the configured fire interval used as timing tolerance (for example, 0.05 allows up to 5% early, capped below).
  • MaxFireIntervalToleranceSeconds: Hard cap for fire interval tolerance in seconds; lower for strict cadence validation, raise slightly if full-auto fire still feels uneven under load.
  • bDefaultCadenceAllowCatchUp: Project default for cadence catch-up behavior.
  • DefaultCadenceMaxCatchUpShots: Project default cap for cadence catch-up shots.
  • bEnableLagCompensation: Enables server-side rewind for hit validation.
  • LagCompMaxRewindSeconds: Maximum rewind window in seconds.
  • LagCompSnapshotRateHz: Snapshot capture rate for rewind targets.
  • LagCompMaxTargetsPerTrace: Cap on rewind targets per trace segment.
  • LagCompQueryRadius: Sphere radius in cm for selecting rewind targets near a trace segment.
  • LagCompProxyScale: Multiplier applied to rewind target transforms to reduce false negatives.

Diagnostics

Bullet Forge diagnostics are dedicated instrumentation paths for short, targeted captures. They are grouped under Project Settings -> Plugins -> Bullet Forge -> Diagnostics, are off by default, and do not change ballistics, gameplay traces, RPC/delegate signatures, cosmetic ordering, batching, or prediction behavior when disabled.

Use Developer Settings for persistent project defaults and runtime CVars for temporary captures. Boolean diagnostics are OR-gated: a diagnostic is enabled when either its Developer Settings toggle or its CVar is enabled. Threshold CVars override Developer Settings only when set to a non-negative value.

Diagnostics Settings (Developer Settings)

  • bEnableDelayedShotDiagnostics (Diagnostics -> Delayed Shot): Developer Settings equivalent for BulletForge.DelayedShotDiagnostics.
  • bEnableFireCosmeticDiagnostics (Diagnostics -> Fire Cosmetics): Developer Settings equivalent for BulletForge.FireCosmeticDiagnostics.
  • bEnableFireCosmeticVerboseDiagnostics (Diagnostics -> Fire Cosmetics): Developer Settings equivalent for BulletForge.FireCosmeticDiagnosticsVerbose.
  • FireCosmeticLateThresholdSeconds (Diagnostics -> Fire Cosmetics): Developer Settings threshold used when BulletForge.FireCosmeticLateMs is negative. The CVar is in milliseconds and non-negative values override this setting.
  • bSuppressStaleOwnerFireCosmeticMulticasts (Diagnostics -> Fire Cosmetics): Enables owner-side suppression for seeded server multicast muzzle/shell cosmetics that match a locally predicted/accepted fire cosmetic seed already played by the owning client. Defaults to true because it only affects owning-client duplicate/stale cosmetics and leaves remote-client multicast cosmetics and bullet traces unchanged. Runtime override: BulletForge.SuppressStaleOwnerFireCosmeticMulticasts -1|0|1 (-1 uses project settings).
  • StaleOwnerFireCosmeticSuppressWindowSeconds (Diagnostics -> Fire Cosmetics): Optional time expiry for locally played seeded fire cosmetics in the owner-side stale multicast suppression cache. Defaults to 0.0, which means no time expiry; suppression depends on membership in the bounded recent seed cache. Runtime override: BulletForge.StaleOwnerFireCosmeticSuppressWindow <seconds> (negative uses project settings, 0 disables time expiry).
  • MaxRecentFireCosmeticSeeds (Diagnostics -> Fire Cosmetics): Bounded recent seed cache size for owner-side stale multicast suppression. Defaults to 1024 to retain a generous automatic-fire history without relying on a time window; set to 0 to clear/disable the stale-owner seed cache.
  • bEnableTracePressureDiagnostics (Diagnostics -> Trace Pressure): Developer Settings equivalent for BulletForge.TracePressureDiagnostics.
  • bEnableTracePressureVerboseDiagnostics (Diagnostics -> Trace Pressure): Developer Settings equivalent for BulletForge.TracePressureDiagnosticsVerbose.
  • TracePressureLatePlaybackThresholdSeconds (Diagnostics -> Trace Pressure): Developer Settings threshold used when BulletForge.TracePressureLateMs is negative. The CVar is in milliseconds and non-negative values override this setting.
  • bEnableTickDiagnostics (Diagnostics -> Tick): Developer Settings equivalent for BulletForge.TickDiagnostics.
  • bEnableTickDiagnosticsVerbose (Diagnostics -> Tick): Developer Settings equivalent for BulletForge.TickDiagnosticsVerbose.
  • TickDiagnosticsSlowThresholdMs (Diagnostics -> Tick): Developer Settings threshold used when BulletForge.TickDiagnosticsSlowMs is negative. The CVar is in milliseconds and non-negative values override this setting.

Delayed Shot

Delayed-shot diagnostics capture the timing path from a local fire request through batching, RPC receive, server validation, deferred prediction, and scheduled impact playback. Use them when you need to compare weapon cadence against request queueing, network arrival, server fire-rate validation, or impact visual/audio scheduling. When enabled, Bullet Forge logs low-noise timing messages with the BFDelayedShot prefix.

Enable diagnostics in either place:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Delayed Shot -> bEnableDelayedShotDiagnostics
  • Runtime console CVar: BulletForge.DelayedShotDiagnostics 1 (use 0 to disable)

Key fields include FireBullet entry/interval decisions, fire batch enqueue/flush decisions, deferred prediction queue enqueue/process age, server direct/batch receive validation, ProcessServerBulletFire start timing, and scheduled impact visual/audio playback delay. Impact records include trace seed/id, fire time, impact time, computed delay, current server/world time, and whether playback was immediate or scheduled. Queue or playback delays above 0.25 seconds are marked with HIGH_DELAY_GT_0_25S to make late events easy to filter. Use these logs with stat BulletForge or stat net when comparing cadence, batching, and cosmetic scheduling.

Example capture sequence:

stat unit
stat BulletForge
stat net
BulletForge.DelayedShotDiagnostics 1
csvprofile start -filename=BF_DelayedShot
// Run a fixed weapon cadence capture, then allow scheduled cosmetics to finish.
csvprofile stop
BulletForge.DelayedShotDiagnostics 0

Trace Pressure

Trace pressure diagnostics summarize active trace lifetime, trace die-off bursts, pending impact visual/audio playback pressure, tracer visual follower backlog/lifetime, visual component churn, FX scheduling, fire-batch distribution, and passive trace subpath timing. They emit one low-noise summary per second with the BFTracePressure prefix and write CSV counters under the BulletForge category.

Enable summary diagnostics in either place:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Trace Pressure -> bEnableTracePressureDiagnostics
  • Runtime console CVar: BulletForge.TracePressureDiagnostics 1 (use 0 to disable)

For temporary captures, leave the Project Settings toggle off and use the CVar so the capture can be disabled from the console. The Project Settings toggle and CVar are OR-gated: if the project setting is enabled, setting BulletForge.TracePressureDiagnostics 0 does not fully disable diagnostics until the project setting is turned off.

Enable verbose per-event diagnostics only when needed:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Trace Pressure -> bEnableTracePressureVerboseDiagnostics
  • Runtime console CVar: BulletForge.TracePressureDiagnosticsVerbose 1 (requires trace pressure diagnostics)

Configure the late scheduled-playback threshold:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Trace Pressure -> TracePressureLatePlaybackThresholdSeconds (default 0.25)
  • Runtime console CVar: BulletForge.TracePressureLateMs 250 (milliseconds; negative values use project settings)

Key counters include:

  • ActiveTraces, ActiveTracePeak, TraceStarts, TraceRemovals, removals this tick, max removals in one tick, active trace count before/after the current removal burst, removal reasons, average trace lifetime, and maximum trace lifetime.
  • ActiveVisual, ActiveVisualPeak, visual follower starts/destroys, follower lifetime average/max, tracer segment count, active follower segment backlog, peak backlog, segments per follower, pending tracer segment count, and pending tracer peak.
  • Niagara and bullet mesh create/destroy counts plus approximate active counts and peaks for components created by Bullet Forge visual followers. These are component-local counters, not global UObject scans.
  • Muzzle, shell, impact visual, and impact audio immediate/scheduled counts.
  • Pending scheduled impact visual/audio count, average/max pending age, scheduled playback count, late playback count over the configured threshold, and average/max lateness at playback time.
  • Fire batch send count, total requests sent in batches, average batch size, maximum batch size, server batch RPC receive count, server batch request count, and direct fire RPC receive count.
  • Passive subpath metrics for active trace line-query count/time, trace segment count/time, hit handling time, penetration backtrace query count/time, ricochet check/success count and time, and flyby overlap count/time.

Example profiling sequence:

stat unit
stat BulletForge
stat net
BulletForge.TracePressureDiagnostics 1
BulletForge.TracePressureLateMs 250
csvprofile start -filename=BF_TracePressure
// Run a fixed sustained-fire capture, preferably 5-10 seconds with the same weapon, map, and net mode each run.
csvprofile stop
BulletForge.TracePressureDiagnostics 0
BulletForge.TracePressureDiagnosticsVerbose 0

For comparable captures, keep weapon RPM, map, net mode, and capture duration consistent. Compare active trace peak, removal bursts, trace lifetimes, pending scheduled impact age, late playback counts/lateness, active visual followers, follower backlog, pending tracer segments, Niagara/bullet mesh active peaks, fire-batch average/max size, and passive subpath timing before tuning trace lifetime, visual pooling, or batching.

The diagnostics intentionally do not perform expensive global UObject scans every tick. Component counts are approximate for Bullet Forge-created Niagara and bullet mesh components only; if another system destroys those components independently, the local active count can be conservative until the visual follower is cleaned up.

Tick and Subpath Performance

Tick diagnostics split BulletForge/GameThread/Tick into component subpaths so captures can identify whether time is spent in active trace processing, visual trace update, deferred prediction, cadence pumping, cleanup/optimization, fire/tracer batch flushes, pruning, or Super::TickComponent. Summary logs use the BFTickDiag prefix and are emitted at most once per second plus any tick at or above the configured slow threshold. Verbose per-slow-trace detail is separately gated.

Enable summary diagnostics in either place:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Tick -> bEnableTickDiagnostics
  • Runtime console CVar: BulletForge.TickDiagnostics 1 (use 0 to disable)

For temporary captures, leave the Project Settings toggle off and use the CVar so the capture can be disabled from the console. The Project Settings toggle and CVar are OR-gated: if the project setting is enabled, setting BulletForge.TickDiagnostics 0 does not fully disable diagnostics until the project setting is turned off.

Configure the slow threshold:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Tick -> TickDiagnosticsSlowThresholdMs
  • Runtime console CVar: BulletForge.TickDiagnosticsSlowMs 10.0 (set to another millisecond value as needed; negative values use project settings)

Enable verbose per-slow-trace diagnostics only when needed:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Tick -> bEnableTickDiagnosticsVerbose
  • Runtime console CVar: BulletForge.TickDiagnosticsVerbose 1 (requires tick diagnostics)

Key counters include:

  • Tick total milliseconds and per-subpath milliseconds for active trace processing, visual trace update, deferred prediction, cadence, cleanup, pruning, optimization, fire batch flush, tracer batch flush, and Super::TickComponent.
  • Active trace and visual trace start/end/peak counts for the tick.
  • Deferred prediction queued/processed counts and fire batch flush/request counts.
  • Traces processed, trace segments and segment time, collision query count and line-query time, hits processed, hit handling time, penetration backtrace query count/time, ricochet check/success count and time, flyby overlap count/time, cleanup removals, and max single-trace milliseconds with trace id, seed, age, distance, segments, queries, and hits.

Example tick capture sequence:

stat unit
stat BulletForge
BulletForge.TickDiagnosticsSlowMs 10
BulletForge.TickDiagnostics 1
csvprofile start -filename=BF_TickDiagnostics
// Run a fixed sustained-fire capture, preferably 5-10 seconds with the same weapon, map, and net mode each run.
csvprofile stop
BulletForge.TickDiagnostics 0
BulletForge.TickDiagnosticsVerbose 0

If a summary points to trace processing and you need per-slow-trace details, repeat a short capture with:

BulletForge.TickDiagnostics 1
BulletForge.TickDiagnosticsVerbose 1

Tick diagnostics are instrumentation-only. They do not change gameplay, ballistics, trace lifetime, batching, networking, visual spawning, or scheduling behavior.

Fire Cosmetics and Stale Cosmetic Suppression

Fire cosmetic diagnostics are off by default and are intended for short captures where muzzle flash, weapon sound, or shell ejection appears late or without a matching bullet. Logs use the BFFireCosmetic prefix and correlate Bullet Forge delegate broadcasts (SpawnWeaponEffects and SpawnShellEffects) with accepted fire records by trace seed/resolved seed. These diagnostics do not change firing, prediction, batching, RPC signatures, delegate signatures, or cosmetic ordering.

Enable summary diagnostics in either place:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Fire Cosmetics -> bEnableFireCosmeticDiagnostics
  • Runtime console CVar: BulletForge.FireCosmeticDiagnostics 1 (use 0 to disable)

Enable verbose per-event diagnostics only for narrow capture windows:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Fire Cosmetics -> bEnableFireCosmeticVerboseDiagnostics
  • Runtime console CVar: BulletForge.FireCosmeticDiagnosticsVerbose 1 (requires fire cosmetic diagnostics)

Configure the late muzzle/shell threshold:

  • Project Settings -> Plugins -> Bullet Forge -> Diagnostics -> Fire Cosmetics -> FireCosmeticLateThresholdSeconds (default 0.25)
  • Runtime console CVar: BulletForge.FireCosmeticLateMs 250 (milliseconds; negative values use project settings)

What is measured:

  • FireBullet entry, accepted-shot, and rejected-shot events with trace seed, resolved seed when known, fire time, net mode, role, pending fire queue size, and known reject reasons such as NullSettings, local/server fire interval rejection, invalid batch shape, or local cosmetic null settings.
  • Accepted-shot records are kept briefly by original trace seed and resolved seed, with source labels such as FireBullet, ServerFireBullet, and ServerFireBulletBatch.
  • Server/batch path events include ServerFireBullet_Implementation, ServerFireBulletBatch_Implementation, and ProcessServerBulletFire starts, plus whether the trace seed matched an accepted-shot record on that machine.
  • Local predicted cosmetics include TriggerLocalCosmeticFire, muzzle/shell delegate broadcasts, accepted-shot match state, cosmetic age since fire time, and late local cosmetic counts over the threshold.
  • Deferred prediction includes queued and processed records, age when processed, and whether the processed item can lead to local cosmetics via the active prediction mode.
  • Server multicast cosmetics include MulticastSpawnMuzzleEffects_Implementation, MulticastSpawnShellEffects_Implementation, and MulticastSpawnImpactEffects_Implementation, owner suppression decisions (PredictedLocalCosmetic, PredictedImpactMatch, StaleSimulatedProxyImpactMulticast, NotRelevant, or dedicated server skip), accepted-record matching, age/lateness, role/net mode, and likely duplicate/replay flags when a late multicast arrives for a seed that already had a local predicted broadcast.
  • Owning-client stale server multicast suppression includes per-effect counts (SuppressedOwnerMuzzleShell, SuppressedOwnerImpactVisualAudio), reason counts (PredictedMatch, StaleSeenSeed, OwnerLateMulticast), recent-seed count, max suppressed age, owner/local role/net mode, and whether the optional suppression window is expiring entries. Suppression happens before SpawnWeaponEffects / SpawnShellEffects delegate broadcasts and before owner-local SpawnImpactEffects / SpawnImpactAudioEffect dispatch for matching seeded impact multicasts.
  • Simulated-proxy impact multicast suppression is separate from owner-local suppression: on clients with simulated-proxy role, only impact visual/audio dispatch is skipped when ServerTime - FireTime - ImpactTime exceeds the configured fire cosmetic late threshold. Non-stale simulated-proxy impact multicasts continue to broadcast and schedule impact visual/audio normally; server traces, damage, ballistics, RPC/delegate signatures, and batching are unchanged.
  • Every Bullet Forge muzzle/shell delegate broadcast is classified as LocalPredicted, ServerMulticast, ServerAuthority, or UnknownLegacy where possible. Broadcasts without a matching accepted-shot record are counted and logged as unmatched.

CSV counters are written under the BulletForge category while diagnostics are enabled, including accepted/rejected counts, local/multicast/server-authority broadcast counts, unmatched broadcasts, late local/multicast broadcast counts, likely duplicate/replay counts, max local/multicast cosmetic age, deferred prediction max age, and accepted-record count. Owner-side stale multicast suppression adds CSV counters for suppressed muzzle/shell counts, suppressed impact visual/audio counts, suppression reason counts, recent seed count, and max suppressed owner multicast age. Simulated-proxy stale impact suppression adds separate CSV counters for suppressed impact visual/audio counts plus max suppressed simulated-proxy impact lateness.

Example capture sequence:

stat unit
stat BulletForge
stat net
BulletForge.FireCosmeticDiagnostics 1
BulletForge.FireCosmeticLateMs 250
BulletForge.SuppressStaleOwnerFireCosmeticMulticasts -1
BulletForge.StaleOwnerFireCosmeticSuppressWindow 0
csvprofile start -filename=BF_FireCosmetics
// Optional for narrow captures only:
// BulletForge.FireCosmeticDiagnosticsVerbose 1
// Run a sustained-fire capture, then wait a few seconds for delayed cosmetics to finish.
csvprofile stop
BulletForge.FireCosmeticDiagnosticsVerbose 0
BulletForge.FireCosmeticDiagnostics 0

Interpretation guide:

  • Late LocalPredicted broadcasts with matching accepted records point at local predicted cosmetics or deferred prediction processing falling behind.
  • Late ServerMulticast broadcasts with prior local broadcasts and DuplicateReplayLikely=true point at stale multicast replay or owner suppression that did not match.
  • MuzzleMulticast Suppressed / ShellMulticast Suppressed with reason PredictedMatch means the normal immediate predicted-cosmetic suppression path handled the owning-client duplicate. ImpactMulticast Suppressed ... Reason=PredictedImpactMatch means the existing predicted-impact reconciliation handled a matching impact. ImpactVisualMulticast Suppressed / ImpactAudioMulticast Suppressed with reason StaleSeenSeed or OwnerLateMulticast means the bounded recent-seed cache suppressed a late/replayed owner-local impact visual/audio dispatch for a shot already played locally. ImpactMulticast Suppressed ... Reason=StaleSimulatedProxyImpactMulticast means a client simulated-proxy received an impact multicast after ServerTime - FireTime - ImpactTime exceeded the fire cosmetic late threshold, so only that stale visual/audio dispatch was skipped.
  • If delayed owner-local muzzle/shell or impact cosmetics continue with suppression enabled, check that stale multicast logs include a nonzero trace seed and that RecentSeeds has not been pruned by MaxRecentFireCosmeticSeeds (or by StaleOwnerFireCosmeticSuppressWindowSeconds only when it is set above 0). Unseeded multicast paths are not suppressed by the recent-seed cache; only the existing unseeded fallback window applies.
  • UnmatchedBroadcasts or MatchedAccepted=false logs mean a muzzle/shell delegate broadcast could not be tied to a Bullet Forge accepted shot on that machine. Remote clients may legitimately lack local accepted-shot records for server multicasts; owning-client unmatched local predicted broadcasts usually deserve closer inspection.
  • DeferredPrediction Processed ... Late=true LeadsToCosmetics=true indicates deferred prediction could be producing late local cosmetics even though the server request was already sent/enqueued earlier.
  • If BFFireCosmetic shows no delegate broadcast for the observed visual/audio effect, the effect is likely coming from project Blueprint/timer logic outside Bullet Forge delegate broadcasts. Bullet Forge cannot directly observe downstream Blueprint handler execution; it instruments immediately before each Bullet Forge delegate broadcast.

Fire Batching Tuning

  • FireBatchInterval: Lower values reduce perceived latency for other clients but increase RPC frequency.
  • MaxFireRequestsPerBatch: Keep this small (2-6) for full-auto weapons to avoid bursty impact playback.
  • Best practice: Keep FireBatchInterval at or below the fastest weapon's GetFireIntervalSeconds to keep added batching delay bounded. Lower values reduce latency at higher RPC cost.
  • Measure first: Use trace pressure diagnostics to confirm actual fire batch size distribution before increasing FireBatchInterval. A 600 RPM weapon has a 0.100s shot interval, so a 0.050s fire-batch window usually sends one request per batch by design.

Lag Compensation

Bullet Forge can optionally rewind registered targets on the server to validate hits at the client-reported fire timestamp. This only affects hit validation; damage remains server-authoritative.

Setup (recommended):

  1. Add UBulletForgeRewindComponent to actors you want to rewind.
  2. Leave auto-register enabled and ensure your pawn has collision components (capsule and/or skeletal mesh).
  3. Enable bEnableLagCompensation and tune the snapshot rate and rewind window.

Manual setup:

  1. Add UBulletForgeRewindComponent.
  2. Call RegisterRewindTarget for each collision component you want rewound.
  3. Call AutoRegisterRewindTargets if you want to refresh after runtime component changes.

Auto-registration behavior:

  • Capsules: Registers the first UCapsuleComponent on the owner if enabled.
  • Skeletal meshes: Registers every USkeletalMeshComponent on the owner. If the mesh uses a physics asset for collision, rewinding the component rewinds those bodies too.
  • Static meshes: Registers every UStaticMeshComponent on the owner if enabled.

Notes:

  • Rewind targets are restored immediately after each trace segment.
  • Keep auto-register enabled for fast onboarding; use manual registration for fine-grained control.

Performance & Profiling

Runtime Stats

Bullet Forge exposes a BulletForge stat group and per-tick counters:

  • stat BulletForge to view cycle timings and counters.
  • Key counters include active traces, traces per tick, tracer batches/segments, checkpoints, and culled effects.

Tip: Use stat unit or stat game alongside stat BulletForge to correlate BulletForge work with overall frame cost.

Performance Test Checklist

  1. Use PIE with 2+ clients (Listen Server or Play As Client).
  2. Use a fixed RPM weapon and fire a 5-10s full-auto burst.
  3. Run stat net and compare RPC counts/bandwidth before and after batching.
  4. Run stat BulletForge to confirm trace and tick costs stay stable.
  5. Use csvprofile start/stop to capture a comparable run for analysis.
  6. If lag compensation is enabled, add Net PktLag=100 and confirm hit validation remains consistent.
  7. Optional: add Net PktLoss=2 to check for bursty impact playback.

CSV Profiler

Bullet Forge emits CSV stats under the BulletForge category.

  • Start capture: csvprofile start
  • Stop capture: csvprofile stop
  • Results: Saved/Profiling/CSV (filter for the BulletForge category).

Optional:

  • Capture a fixed number of frames: csvprofile frames=300
  • Enable GPU CSV stats: r.GPUCsvStatsEnabled 1

When BulletForge.TracePressureDiagnostics is enabled, extra CSV counters are emitted for active trace peaks, trace starts/removals, visual trace churn, Niagara/bullet mesh creation, and fire-batch distribution. When BulletForge.TickDiagnostics is enabled, extra CSV counters are emitted for tick total/subpath timings, trace segment/collision/hit counts, and max single-trace timing. These counters are intentionally disabled with their diagnostics gates during normal play.

Net Profile

Use Unreal's net profiler to measure RPC counts and bandwidth:

  • Start capture: net profile
  • Stop capture: net profile
  • Look for MulticastSpawnTracerBatch and ClientReceiveTraceCheckpoint to evaluate tracer batching and checkpoints.

Tip: stat net provides a quick HUD view, but per-RPC filtering requires the net profiler (.nprof) in NetworkProfiler.exe.

Output files are written to Saved/Profiling/ with a .nprof extension and can be opened using NetworkProfiler.exe in the engine binaries.

UMG Perf Widget (C++)

UBulletForgePerfWidget is a lightweight UUserWidget that reads counters from a UBulletForgeWeaponComponent.

  1. Create a Widget Blueprint based on UBulletForgePerfWidget.
  2. Bind text fields to the widget's properties (ActiveTraces, TracesProcessed, TracerBatchesSent, etc.).
  3. Call SetSourceComponent with the weapon component you want to monitor (HUD, PlayerController, or Pawn).
  4. Leave bAutoRefresh enabled to update every tick, or call RefreshSnapshot manually.
  5. Add the widget to the viewport using Create Widget + Add to Viewport (Blueprint or C++).

The widget now exposes lag compensation counters (shots, targets rewound, clamped timestamps, rewind time in ms, registered targets, and history frames). stat BulletForge also includes lag comp counters.

Prediction Modes

Use PredictionMode on UBulletForgeWeaponComponent to tune client-side prediction:

  • None: No client simulation or predicted impacts; server multicast drives effects and checkpoints are ignored.
  • Partial Forward: Client predicts local muzzle/shell cosmetics and immediate cosmetic impact effects; server authoritative traces drive gameplay.
  • Full: Client runs full trace prediction and applies server checkpoints for reconciliation.

Why change modes:

  • None: Lowest client CPU and simplest inspection path; useful for low-end clients or when prediction is undesirable (cinematics, tools).
  • Partial Forward: Good balance when you want immediate cosmetic feedback but prefer server-only gameplay traces to avoid divergence.
  • Full: Best responsiveness for fast weapons and high-latency sessions; requires prediction/reconciliation tolerance.

Runtime guidance:

  • Consider switching to Partial or None during performance drops, scripted sequences, or modes where prediction artifacts are unacceptable.
  • Use Full for competitive or high-latency scenarios where responsiveness matters most.
  • Keep Full prediction on Deferred execution for automatic weapons unless you have measured and accepted the synchronous cost of Immediate mode.

Runtime API:

  • SetPredictionMode updates the mode at runtime; PredictionMode itself is default-only.
  • PredictionExecutionMode controls when the client does local prediction work. ProjectDefault uses the developer setting, which defaults to Deferred and resolves to Deferred if unset/invalid.
  • Immediate keeps legacy same-call-stack local prediction. Deferred queues local prediction after the server request path and processes up to MaxDeferredPredictedShotsPerTick queued shots per tick.

Physics & Ballistics Deep Dive

Ballistic Tracing (Trajectory Simulation)

Bullet Forge uses a segmented simulation approach to ensure high accuracy and collision reliability even at high velocities.

  • Initial Overlap Detection: Traces are configured to find initial overlaps (bFindInitialOverlaps), ensuring hits are detected even if a trace starts inside a collision primitive (e.g., after penetrating a surface).
  • Immediate Muzzle Trace: Bullets perform an immediate sub-frame trace upon firing to catch targets extremely close to the muzzle before the first tick.
  • Multi-Hit Segment Processing: If a bullet penetrates an object and hits another in the same segment, the second hit is processed immediately in the same frame for perfect consistency.
  • Gravity Application: Applied every tick as a constant acceleration: Velocity = Velocity + (Gravity * DeltaTime) Default Gravity: -980.665 cm/s²
  • Drag Model (G1): Energy loss due to air resistance is calculated using the bullet's cross-sectional area, velocity, and air density: DragForce = 0.5 * AirDensity * Velocity² * DragCoefficient * Area EnergyLoss = DragForce * Distance DragCoefficient = 0.5 / BallisticCoefficient
  • Velocity Update: After energy loss (drag or impact), velocity is recalculated from kinetic energy: Velocity = sqrt(2 * Energy / Mass)

Penetration System

The penetration system determines if a bullet can pass through an object and how much energy it loses in the process.

  • Material Resistance: Calculated based on thickness, hardness, and density: Resistance = Thickness * Hardness * Density
  • Hardness Calculation: Derived from physical material density or interface overrides: Hardness = (Density / HardnessDivisor) + HardnessModifier
  • Energy Loss: If BulletPower > Resistance, the bullet penetrates: EnergyLoss = Resistance * PenetrationEnergyLossFactor
  • Penetration Depth: The distance the bullet travels through the material. By default, this is calculated as the distance between the entry point and the exit point: PenetrationDepth = Distance(EntryLocation, ExitLocation) The exit point is derived from the material's calculated thickness plus a tiny offset (0.1cm) to ensure the next trace starts outside the component without skipping thin objects.
  • Energy Retention: Bullets retain 100% of their energy when penetration is forced via IBulletForgeInterface or if the calculated material resistance is zero.
  • Exit Logic: The system traces through the material to find the exit point, applying the calculated energy loss and updating the trajectory for subsequent traces. If another object is detected immediately behind the exit point, it is processed in the same frame.

Ricochet Logic

Ricochets occur based on the incidence angle and material properties.

  • Incidence Angle: Calculated relative to the surface normal (0° is perpendicular, 90° is parallel/grazing).
  • Probability Roll: Ricochets typically occur at shallow (grazing) angles: AngleFactor = AngleFactorModifier * ((IncidenceAngle - 60°) / 30°) RicochetChance = BaseProbability * AngleFactor
  • Energy Retention: Upon a successful ricochet roll: ReflectedDirection = IncomingDirection - 2 * (IncomingDirection ⋅ Normal) * Normal EnergyAfter = EnergyBefore - (EnergyBefore * RicochetEnergyRetentionFactor)

Default Behaviors & Overrides

  • Fallbacks: If an actor doesn't implement IBulletForgeInterface, the system uses global BulletForgeDeveloperSettings for density (250 kg/m³), thickness (10 cm), and hardness.
  • Interface Control: Implement IBulletForgeInterface to:
    • ForcePenetration or ForceRicochet.
    • Provide dynamic MaterialThickness and MaterialHardness.
    • Custom PenetrationResistance calculations.

Overridable Features & Examples

The IBulletForgeInterface allows for granular control over how projectiles interact with specific actors. This is crucial for implementing specialized armor, destructible environments, or gameplay-specific penetration rules.

Penetration Resistance (GetPenetrationResistance)

By default, resistance is calculated as Thickness * Hardness * Density. Overriding this allows you to define custom logic for specific actors, such as "Magic Shields" or "Composite Armor" that doesn't follow standard physical rules.

Effect on Trajectory:

  • If Power > Resistance: The bullet continues with reduced energy.
  • If Power <= Resistance: The bullet stops (or attempts to ricochet).
  • Damage Potential: Higher resistance leads to higher EnergyLoss, which significantly reduces the damage dealt to targets hit further along the trajectory.

C++ Example (Composite Armor):

float AMyArmorActor::GetPenetrationResistance_Implementation(float Thickness, float Hardness, float Density, const FHitResult& Hit, const FBulletTrace& Trace) const
{
    // Composite armor provides 2x resistance against small caliber rounds
    float BaseResistance = Thickness * Hardness * Density;
    if (Trace.Settings->BulletDiameter < 7.62f)
    {
        return BaseResistance * 2.0f;
    }
    return BaseResistance;
}

Dynamic Armor Degradation Example

A common requirement is to have armor become less effective as it takes damage. You can achieve this by combining a state variable (e.g., ArmorHealth) with the GetPenetrationResistance override.

Key Concept: Use a "Resistance Intensity" multiplier to control how much the armor health affects the final resistance.

C++ Example:

// In your Actor header (.h)
UPROPERTY(EditAnywhere, Category = "Armor")
float ArmorHealth = 100.0f;

UPROPERTY(EditAnywhere, Category = "Armor")
float MaxArmorHealth = 100.0f;

/* Higher values make the resistance drop more sharply as health decreases */
UPROPERTY(EditAnywhere, Category = "Armor")
float ResistanceDegradationIntensity = 1.0f;

// In your Actor implementation (.cpp)
float AMyCharacter::GetPenetrationResistance_Implementation(float Thickness, float Hardness, float Density, const FHitResult& Hit, const FBulletTrace& Trace) const
{
    float BaseResistance = Thickness * Hardness * Density;
    
    // Calculate health ratio (0.0 to 1.0)
    float HealthRatio = FMath::Clamp(ArmorHealth / MaxArmorHealth, 0.0f, 1.0f);
    
    // Apply intensity curve (e.g., using power function for non-linear degradation)
    float EffectiveMultiplier = FMath::Pow(HealthRatio, ResistanceDegradationIntensity);
    
    return BaseResistance * EffectiveMultiplier;
}

Blueprint Logic:

  1. Variables: Add ArmorHealth (Float), MaxArmorHealth (Float), and ResistanceIntensity (Float) to your Blueprint.
  2. Override: In the Interfaces section, double-click Get Penetration Resistance.
  3. Graph:
    • Divide ArmorHealth by MaxArmorHealth.
    • Pass the result into a Power node (Base), with ResistanceIntensity as the Exp.
    • Multiply the original Thickness * Hardness * Density calculation by the result of the Power node.
    • Return the final value.

Advanced Penetration: Absorption Rate & Damage Scaling

In some scenarios, you might want armor that focuses on energy dissipation rather than just physical thickness. This is handled via the Absorption Rate.

Defining Absorption Rate:

  • Absorption Rate (J/kg): Defines the amount of energy (Joules) the material can dissipate per kilogram of mass (e.g., 5000 J/kg).
  • High Absorption Rate: Armor dissipates energy efficiently. Bullets lose energy quickly, converting it into harmless heat, deformation, and fracture. This results in higher penetration resistance.
  • Low Absorption Rate: Armor transfers energy poorly. Bullets "punch through" with more remaining energy. This results in lower penetration resistance.

Energy Conversion & Durability: The total energy dissipated by the armor is calculated based on its mass, absorption rate, and current durability. This energy is converted into armor damage (durability loss), while any remaining energy continues as bullet kinetic energy.

C++ Example (Energy Dissipating Armor with Durability):

float AAdvancedArmor::GetPenetrationResistance_Implementation(float Thickness, float Hardness, float Density, const FHitResult& Hit, const FBulletTrace& Trace) const
{
    // 1. Calculate base physical resistance
    float PhysicalResistance = Thickness * Hardness * Density;
    
    // 2. Calculate dynamic Impact Area based on bullet diameter
    // Using the helper library:
    float ImpactArea = UBulletForgeFunctionLibrary::CalculateImpactArea(Trace.Settings->BulletDiameter);
    
    // 3. Calculate Material Mass in the impact zone
    // Mass (kg) = Area (m²) * Thickness (m) * Density (kg/m³)
    float MaterialMass = ImpactArea * (Thickness / 100.0f) * Density;
    
    // 4. Energy Dissipation Capacity (Joules)
    // Scale dissipation by current armor health/durability
    float HealthRatio = FMath::Clamp(ArmorHealth / MaxArmorHealth, 0.0f, 1.0f);
    float DissipationCapacity = MaterialMass * AbsorptionRate * HealthRatio;
    
    // 5. Bullet Damage Factor
    // High-damage bullets can overwhelm the armor's ability to dissipate energy
    float BulletOverwhelmFactor = FMath::Max(1.0f, Trace.Settings->BaseDamage / 100.0f);
    
    // Final Resistance increases with DissipationCapacity but decreases if the bullet overwhelms it
    return (PhysicalResistance + DissipationCapacity) / BulletOverwhelmFactor;
}

Blueprint Example:

  1. Variables: Add AbsorptionRate (Float, default 5000.0), ArmorHealth (Float), and MaxArmorHealth (Float).
  2. Override: Get Penetration Resistance.
  3. Logic:
    • Calculate ImpactArea using Calculate Impact Area node (pass BulletDiameter).
    • Calculate MaterialMass = ImpactArea * (Thickness / 100.0) * Density.
    • Calculate HealthRatio = ArmorHealth / MaxArmorHealth.
    • Calculate Dissipation = MaterialMass * AbsorptionRate * HealthRatio.
    • Calculate BasePhysical = Thickness * Hardness * Density.
    • Calculate OverwhelmFactor = Max(1.0, BaseDamage / 100.0).
    • Return (BasePhysical + Dissipation) / OverwhelmFactor.

Penetration Power Modifier (GetPenetrationPowerModifier)

This function adds a power boost (or penalty) to the bullet's current energy only for the comparison against the current hit's resistance. It does not permanently change the bullet's energy unless penetration succeeds and the standard energy loss is applied.

Effect on Trajectory:

  • It effectively "helps" or "hinders" the bullet's ability to pass through a specific surface without altering its long-term ballistic curve.

Blueprint Example (Incendiary Weak Point):

  1. Implement IBulletForgeInterface in your Blueprint.
  2. Override GetPenetrationPowerModifier.
  3. Logic: If (Trace.Settings.Tags.Contains("Incendiary")) return 500.0f; else return 0.0f;

Forced Interactions (ForcePenetration / ForceRicochet)

These bypass all mathematical checks.

  • ForcePenetration: Useful for "Ghost Rounds" or cinematic sequences where a bullet must pass through regardless of thickness.
  • ForceRicochet: Useful for specialized "Deflector Shields" or surfaces designed to bounce bullets regardless of the incidence angle.

Overriding Penetration Depth (GetPenetrationDepth)

While the default behavior calculates depth based on physical exit points, you can override this to simulate materials that catch bullets or vary depth based on projectile properties like damage.

Effect on Events: The value returned here is passed directly to the FPenetrationEvent and can be used for UI effects, decal placement, or custom gameplay logic.

C++ Example (Damage-Based Penetration Depth):

float AMyActor::GetPenetrationDepth_Implementation(const FHitResult& Hit, const FBulletTrace& Trace, float DefaultPenetrationDepth) const
{
    // Higher damage bullets penetrate deeper, but we never exceed the physical thickness
    float DamageFactor = Trace.Settings->BaseDamage / 50.0f; // Scale based on 50 base damage
    float TargetDepth = 10.0f * DamageFactor; 
    
    // Return the lesser of our target depth or the physical thickness
    return FMath::Min(TargetDepth, DefaultPenetrationDepth);
}

Blueprint Example:

  1. Implement IBulletForgeInterface.
  2. Override Get Penetration Depth.
  3. Logic: Multiply Bullet Settings -> Base Damage by a scale factor, use Min with Default Penetration Depth, and return.

Impact on Damage and Energy

Energy loss during penetration is calculated as: EnergyLoss = Resistance * PenetrationEnergyLossFactor (from UBallisticsSettings).

The bullet's velocity is then updated: Velocity = sqrt(2 * (CurrentEnergy - EnergyLoss) / Mass)

Example Scenario:

  • A bullet with 1000J hits a plate with 400 Resistance.
  • EnergyLossFactor is 1.0.
  • The bullet exits with 600J.
  • A target behind the plate receives a HitEvent with CurrentEnergy = 600J. If damage is scaled by energy, the lethality is effectively reduced by 40%.

Damage & Armor Implementation Guidelines

Bullet Forge is a physics and ballistics simulation system. It does not apply damage to Actors automatically. Instead, it provides the necessary events and data for your game's damage system to react to.

Best Place to Apply Damage

The best place to apply damage to a character or object is by listening to the multicast delegates on the UBulletForgeReactiveComponent attached to the target actor.

  • OnHitEvent: This is the primary event for applying damage. It provides a FHitEvent struct containing the CurrentEnergy of the bullet at the moment of impact and the EnergyLoss resulting from the hit.
  • OnPenetrationEvent: Triggered when a bullet passes through. Use this if you want to apply "exit wound" logic or specific penetration-based damage.

Recommended Workflow:

  1. Attach UBulletForgeReactiveComponent to your Actor (e.g., Character).
  2. On the Server, bind a function to OnHitEvent.
  3. In the bound function, use UBulletForgeFunctionLibrary::CalculateDamageFromHitEvent to get the physics-scaled damage.
  4. Call UGameplayStatics::ApplyPointDamage or your custom damage function.

Best Place to Calculate Armor Damage

Armor damage (durability loss) should be calculated based on the energy the bullet spent trying to overcome the armor.

  • Option A: Reactive Events (Recommended for State Changes): Listen to OnHitEvent or OnPenetrationEvent and use EnergyLoss (in FPenetrationEvent) or a calculation based on CurrentEnergy to reduce armor durability.
  • Option B: Interface Overrides (For Dynamic Calculations): If your armor's effectiveness changes based on its current durability, override GetPenetrationResistance in IBulletForgeInterface. You can return a lower resistance value as the armor gets damaged.

Example: Armor Durability Logic

void AMyCharacter::HandleBulletHit(const FHitEvent& HitData)
{
    if (!HasAuthority()) return;

    // 1. Armor Durability Loss
    // Use the EnergyLoss provided by the system. This accounts for 
    // physical resistance, absorption rates, and impact area.
    float EnergyToDurabilityFactor = 0.05f; // Configurable scaling
    float DurabilityLoss = HitData.EnergyLoss * EnergyToDurabilityFactor;
    
    CurrentArmorDurability = FMath::Max(0.0f, CurrentArmorDurability - DurabilityLoss);
    
    // 2. Apply Health Damage
    // Use the function library to scale damage based on the bullet's remaining energy
    float FinalDamage = UBulletForgeFunctionLibrary::CalculateDamageFromHitEvent(HitData);
    
    // You might further reduce damage based on current armor status
    if (CurrentArmorDurability > 0) {
        FinalDamage *= 0.2f; // Armor absorbs 80% of health damage while active
    }

    UGameplayStatics::ApplyPointDamage(this, FinalDamage, HitData.HitResult.ImpactNormal * -1.0f, HitData.HitResult, ...);
}

© 2025 Opsidium Designs. All rights reserved.

Clone this wiki locally