-
Notifications
You must be signed in to change notification settings - Fork 0
Core Documentation
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.
- Quick Start / Integration Guide
- What Changed in 1.7.0
- Key Features
- Technical Details
- Core Components
- Configuration
- Networking Model (Deterministic Prediction)
- Diagnostics
- Lag Compensation
- Performance & Profiling
- Prediction Modes
- Physics & Ballistics Deep Dive
- Overridable Features & Examples
- Damage & Armor Implementation Guidelines
-
Setup Weapon: Add
UBulletForgeWeaponComponentto your weapon actor. -
Configure Ammo: Create a
UBallisticsSettingsData Asset for your ammunition. -
Fire: For single/manual shots, call
FireBulleton the weapon component from the owning client or authoritative server, passing in the muzzle transform and your ballistics settings.FireBullethandles prediction, seed generation, batching, and server RPCs. For semi-auto/full-auto/burst fire loops, prefer the cadence engine (StartCadenceFire,StartCadenceFireWithProvider, orTryFireCadenceShot). UseServerFireBulletonly for advanced/manual integrations and provide a nonzeroTraceSeed. -
Setup Targets: For actors that need to react to bullets, add a
UBulletForgeReactiveComponentand bind to its events. ImplementIBulletForgeInterfaceif you need fine-grained control over how the bullet interacts with that specific actor.
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::PredictionExecutionModedefaults toProjectDefault; if the project setting is missing or invalid, Bullet Forge falls back toDeferred. - Client
FireBulletqueues/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
MaxDeferredPredictedShotsPerTickper weapon component. - The cadence engine adds optional semi-auto, full-auto, and burst helpers for stable weapon loops.
- Weapon actors can implement
IBulletForgeCadenceProviderto 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.
- 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
ActiveTracesare 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.
- Language: C++
- Dependencies: Niagara, Physical Materials.
- Network: Supports Client/Server architecture.
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
FireIntervalSecondsor overrideGetFireIntervalSeconds(Settings)for Bullet Forge's low-level validation interval. It is used byFireBulletand cadence clamping, but it is not a full weapon/attachment rate system. -
Prediction Mode: Set
PredictionModeor overrideGetPredictionModeto control client-side prediction (None, Partial Forward, Full). -
Prediction Execution: Set
PredictionExecutionModeto choose whether client prediction runs immediately inFireBulletor is deferred to Bullet Forge tick. Deferred is the default for responsiveness. -
Cadence Engine: Use
StartCadenceFire,StartCadenceFireWithProvider,StopCadenceFire, andTryFireCadenceShotfor semi-auto, full-auto, and burst fire loops that are not coupled to one shot's simulation cost.
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.
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.
Attach this component to actors that should respond to ballistics. It provides several multicast delegates:
-
OnHitEvent: Triggered on any impact. ProvidesCurrentEnergyandEnergyLoss(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.
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.
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.
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.
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 useComputeFireIntervalFromRPM) -
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
FireIntervalSecondsat 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
FireIntervalSecondswhenever it changes. -
Component overrides: If you want Bullet Forge to pull the value dynamically, subclass
UBulletForgeWeaponComponentand overrideGetFireIntervalSecondsin 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:
- Simple path: set the Bullet Forge component's
FireIntervalSecondsfrom your weapon Blueprint's current fire-rate value. - Dynamic path: create a Blueprint subclass of
UBulletForgeWeaponComponent, overrideGetFireIntervalSeconds, 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
FireBatchIntervalto be less than or equal toFireIntervalSecondsto keep added batching delay bounded; lower values reduce latency at higher RPC cost.
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
FireIntervalSecondsuntilStopCadenceFireis called, clamped to low-level validation. -
Burst: fires
BurstCountshots at cadenceFireIntervalSeconds, clamped to low-level validation, then stops.
Usage styles:
-
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();
-
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
IBulletForgeCadenceProvideron the weapon actor that owns the component, then callStartCadenceFireWithProvider. Bullet Forge automatically uses the owner actor interface. BindOnCadenceShotFiredfor accepted shots andOnCadenceFireBlockedfor blocked shots (NoCadenceProvider,NullSettings,FireBulletRejected,BuildRequestFailed, or provider can-fire reasons). -
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.
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:
- Implement
IBulletForgeCadenceProvideron the weapon actor that owns theUBulletForgeWeaponComponent. - On trigger press, create
FBulletForgeCadenceSettingsand callStartCadenceFireWithProvider(Settings). - 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:
-
StartCadenceFireWithProviderstarts the cadence loop from the trigger press. - For each due shot, Bullet Forge calls
BuildBulletForgeFireRequestso it can get current shot data and resolve the effective cadence interval from the request settings. - Bullet Forge calls
CanBulletForgeCadenceFirebefore firing the built request. - If the request is valid, can-fire passes, and
FireBulletaccepts it, Bullet Forge callsHandleBulletForgeCadenceShotFiredand broadcastsOnCadenceShotFired. - If any step blocks, Bullet Forge calls
HandleBulletForgeCadenceFireBlockedand broadcastsOnCadenceFireBlockedwith the reason. -
StopCadenceFireends 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, andFireBulletRejected. -
BuildRequestFailedusually meansBuildBulletForgeFireRequestreturnedfalse; verify the request is filled and the function returnstruewhen valid.
Catch-up behavior:
-
bAllowCatchUp = falseskips missed shots after a hitch and resumes cadence forward. This is the safest default for competitive/full-auto weapons. -
bAllowCatchUp = trueallows bounded recovery after a hitch. -
MaxCatchUpShotscaps 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
FireBulletreturns, so any synchronous prediction/effect work can delay cadence. - Prefer implementing
IBulletForgeCadenceProvideron the weapon actor, then callStartCadenceFireWithProvideron trigger press andStopCadenceFireon trigger release. UseTryFireCadenceShotif you need to keep an existing pump.
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
FireBulletor start cadence from the owning client for player weapons that need responsive local feedback. -
Do call
FireBulletor 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.
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.
-
Create or choose a Niagara System for the tracer. This section documents the setup; it does not create any assets.
-
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.
-
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.TraceStartVector TracerStartParameterName=TraceStartUser.TraceEndVector TracerEndParameterName=TraceEndUser.TraceDirectionVector TracerDirectionParameterName=TraceDirectionUser.TraceLengthFloat TracerLengthParameterName=TraceLengthUser.TraceDurationFloat TracerDurationParameterName=TraceDuration -
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.
-
Assign the Niagara System to
UBallisticsSettings::TracerEffecton the ammo settings data asset. -
Tune
TracerVisualRotationOffsetif the tracer's authored forward axis does not match Bullet Forge's visual travel direction. -
Tune lifetime with
TracerVisualMinLifetimeandTracerVisualMaxLifetime. Keep the range short for fast rounds and increase it only when the visual needs more persistence.
Tracer settings on UBallisticsSettings:
TracerEffectTracerVisualRotationOffset-
TracerStartParameterName(defaultTraceStart) -
TracerEndParameterName(defaultTraceEnd) -
TracerDirectionParameterName(defaultTraceDirection) -
TracerLengthParameterName(defaultTraceLength) -
TracerDurationParameterName(defaultTraceDuration) TracerVisualMinLifetimeTracerVisualMaxLifetime
- Assign a static mesh to
UBallisticsSettings::BulletMesh. - Set
BulletMeshVisualRotationOffsetso 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. - Set
BulletMeshVisualScalefor the spawned mesh size. - Set
BulletMeshVisualMinLifetimeandBulletMeshVisualMaxLifetimeto control how long the mesh proxy remains visible while following the sampled path. - 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.
- Default collision is
- To opt in to cosmetic mesh collision, configure
BulletMeshVisualCollisionProfileNamewith the engine collision preset picker (FCollisionProfileName), then tuneBulletMeshVisualCollisionEnabledandbBulletMeshVisualGenerateOverlapEventslike the Collision section of a default Static Mesh Component in a Blueprint. If the preset isCustom,BulletMeshVisualObjectTypeandBulletMeshVisualCollisionResponsesare also applied. This collision is still visual/cosmetic and does not replace Bullet Forge hit validation or damage.
Bullet mesh settings on UBallisticsSettings:
BulletMeshBulletMeshVisualRotationOffsetBulletMeshVisualScaleBulletMeshVisualMinLifetimeBulletMeshVisualMaxLifetime-
BulletMeshVisualCollisionEnabled(defaultNoCollision) -
BulletMeshVisualCollisionProfileName(FCollisionProfileName, defaultNoCollision) -
bBulletMeshVisualGenerateOverlapEvents(defaultfalse) BulletMeshVisualObjectTypeBulletMeshVisualCollisionResponses
| 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. |
Use PIE to validate the visual setup:
- Start PIE with 2+ players using the project's normal multiplayer test mode.
- Fire a weapon using a
UBallisticsSettingsasset withTracerEffectand/orBulletMeshassigned. - Confirm the tracer and bullet mesh follow bullet drop over distance.
- Fire at a ricochet-capable surface and confirm visuals follow the post-ricochet path sample instead of drawing a straight muzzle-to-impact line.
- Confirm hit, penetration, ricochet, and damage behavior still comes from Bullet Forge traces/reactive events, not from visual mesh collision.
- On remote clients, confirm visuals appear or are intentionally culled by
EffectCullDistance.
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.
MaxTracesPerTick remains the legacy active-trace count cap. Use the gated diagnostics below to measure trace and flyby cost without changing runtime behavior.
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
EffectCullDistanceto 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
SpawnImpactAudioEffectusing 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.
-
bEnableDeterministicPrediction: Enables deterministic random streams so client/server spread and ricochet match. -
DefaultPredictionExecutionMode: Project-wide prediction execution default.Deferredis the default and processes local prediction from Bullet Forge tick;Immediatepreserves legacy same-call-stack prediction. If a component is set toProjectDefaultand this project setting is unavailable or set back toProjectDefault, Bullet Forge resolves toDeferred. -
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.
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.
-
bEnableDelayedShotDiagnostics(Diagnostics -> Delayed Shot): Developer Settings equivalent forBulletForge.DelayedShotDiagnostics. -
bEnableFireCosmeticDiagnostics(Diagnostics -> Fire Cosmetics): Developer Settings equivalent forBulletForge.FireCosmeticDiagnostics. -
bEnableFireCosmeticVerboseDiagnostics(Diagnostics -> Fire Cosmetics): Developer Settings equivalent forBulletForge.FireCosmeticDiagnosticsVerbose. -
FireCosmeticLateThresholdSeconds(Diagnostics -> Fire Cosmetics): Developer Settings threshold used whenBulletForge.FireCosmeticLateMsis 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 totruebecause 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(-1uses project settings). -
StaleOwnerFireCosmeticSuppressWindowSeconds(Diagnostics -> Fire Cosmetics): Optional time expiry for locally played seeded fire cosmetics in the owner-side stale multicast suppression cache. Defaults to0.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,0disables time expiry). -
MaxRecentFireCosmeticSeeds(Diagnostics -> Fire Cosmetics): Bounded recent seed cache size for owner-side stale multicast suppression. Defaults to1024to retain a generous automatic-fire history without relying on a time window; set to0to clear/disable the stale-owner seed cache. -
bEnableTracePressureDiagnostics(Diagnostics -> Trace Pressure): Developer Settings equivalent forBulletForge.TracePressureDiagnostics. -
bEnableTracePressureVerboseDiagnostics(Diagnostics -> Trace Pressure): Developer Settings equivalent forBulletForge.TracePressureDiagnosticsVerbose. -
TracePressureLatePlaybackThresholdSeconds(Diagnostics -> Trace Pressure): Developer Settings threshold used whenBulletForge.TracePressureLateMsis negative. The CVar is in milliseconds and non-negative values override this setting. -
bEnableTickDiagnostics(Diagnostics -> Tick): Developer Settings equivalent forBulletForge.TickDiagnostics. -
bEnableTickDiagnosticsVerbose(Diagnostics -> Tick): Developer Settings equivalent forBulletForge.TickDiagnosticsVerbose. -
TickDiagnosticsSlowThresholdMs(Diagnostics -> Tick): Developer Settings threshold used whenBulletForge.TickDiagnosticsSlowMsis negative. The CVar is in milliseconds and non-negative values override this setting.
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(use0to 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 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(use0to 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(default0.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 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(use0to 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 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(use0to 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(default0.25) - Runtime console CVar:
BulletForge.FireCosmeticLateMs 250(milliseconds; negative values use project settings)
What is measured:
-
FireBulletentry, 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 asNullSettings, 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, andServerFireBulletBatch. - Server/batch path events include
ServerFireBullet_Implementation,ServerFireBulletBatch_Implementation, andProcessServerBulletFirestarts, 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, andMulticastSpawnImpactEffects_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 beforeSpawnWeaponEffects/SpawnShellEffectsdelegate broadcasts and before owner-localSpawnImpactEffects/SpawnImpactAudioEffectdispatch 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 - ImpactTimeexceeds 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, orUnknownLegacywhere 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
LocalPredictedbroadcasts with matching accepted records point at local predicted cosmetics or deferred prediction processing falling behind. - Late
ServerMulticastbroadcasts with prior local broadcasts andDuplicateReplayLikely=truepoint at stale multicast replay or owner suppression that did not match. -
MuzzleMulticast Suppressed/ShellMulticast Suppressedwith reasonPredictedMatchmeans the normal immediate predicted-cosmetic suppression path handled the owning-client duplicate.ImpactMulticast Suppressed ... Reason=PredictedImpactMatchmeans the existing predicted-impact reconciliation handled a matching impact.ImpactVisualMulticast Suppressed/ImpactAudioMulticast Suppressedwith reasonStaleSeenSeedorOwnerLateMulticastmeans the bounded recent-seed cache suppressed a late/replayed owner-local impact visual/audio dispatch for a shot already played locally.ImpactMulticast Suppressed ... Reason=StaleSimulatedProxyImpactMulticastmeans a client simulated-proxy received an impact multicast afterServerTime - FireTime - ImpactTimeexceeded 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
RecentSeedshas not been pruned byMaxRecentFireCosmeticSeeds(or byStaleOwnerFireCosmeticSuppressWindowSecondsonly when it is set above0). Unseeded multicast paths are not suppressed by the recent-seed cache; only the existing unseeded fallback window applies. -
UnmatchedBroadcastsorMatchedAccepted=falselogs 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=trueindicates deferred prediction could be producing late local cosmetics even though the server request was already sent/enqueued earlier. - If
BFFireCosmeticshows 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.
- 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
FireBatchIntervalat or below the fastest weapon'sGetFireIntervalSecondsto 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.
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):
- Add
UBulletForgeRewindComponentto actors you want to rewind. - Leave auto-register enabled and ensure your pawn has collision components (capsule and/or skeletal mesh).
- Enable
bEnableLagCompensationand tune the snapshot rate and rewind window.
Manual setup:
- Add
UBulletForgeRewindComponent. - Call
RegisterRewindTargetfor each collision component you want rewound. - Call
AutoRegisterRewindTargetsif you want to refresh after runtime component changes.
Auto-registration behavior:
-
Capsules: Registers the first
UCapsuleComponenton the owner if enabled. -
Skeletal meshes: Registers every
USkeletalMeshComponenton the owner. If the mesh uses a physics asset for collision, rewinding the component rewinds those bodies too. -
Static meshes: Registers every
UStaticMeshComponenton 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.
Bullet Forge exposes a BulletForge stat group and per-tick counters:
-
stat BulletForgeto 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.
- Use PIE with 2+ clients (Listen Server or Play As Client).
- Use a fixed RPM weapon and fire a 5-10s full-auto burst.
- Run
stat netand compare RPC counts/bandwidth before and after batching. - Run
stat BulletForgeto confirm trace and tick costs stay stable. - Use
csvprofile start/stopto capture a comparable run for analysis. - If lag compensation is enabled, add
Net PktLag=100and confirm hit validation remains consistent. - Optional: add
Net PktLoss=2to check for bursty impact playback.
Bullet Forge emits CSV stats under the BulletForge category.
- Start capture:
csvprofile start - Stop capture:
csvprofile stop - Results:
Saved/Profiling/CSV(filter for theBulletForgecategory).
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.
Use Unreal's net profiler to measure RPC counts and bandwidth:
- Start capture:
net profile - Stop capture:
net profile - Look for
MulticastSpawnTracerBatchandClientReceiveTraceCheckpointto 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.
UBulletForgePerfWidget is a lightweight UUserWidget that reads counters from a UBulletForgeWeaponComponent.
- Create a Widget Blueprint based on
UBulletForgePerfWidget. - Bind text fields to the widget's properties (ActiveTraces, TracesProcessed, TracerBatchesSent, etc.).
- Call
SetSourceComponentwith the weapon component you want to monitor (HUD, PlayerController, or Pawn). - Leave
bAutoRefreshenabled to update every tick, or callRefreshSnapshotmanually. - 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.
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:
-
SetPredictionModeupdates the mode at runtime;PredictionModeitself is default-only. -
PredictionExecutionModecontrols when the client does local prediction work.ProjectDefaultuses the developer setting, which defaults to Deferred and resolves to Deferred if unset/invalid. -
Immediatekeeps legacy same-call-stack local prediction.Deferredqueues local prediction after the server request path and processes up toMaxDeferredPredictedShotsPerTickqueued shots per tick.
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 * AreaEnergyLoss = DragForce * DistanceDragCoefficient = 0.5 / BallisticCoefficient -
Velocity Update: After energy loss (drag or impact), velocity is recalculated from kinetic energy:
Velocity = sqrt(2 * Energy / Mass)
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
IBulletForgeInterfaceor 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.
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) * NormalEnergyAfter = EnergyBefore - (EnergyBefore * RicochetEnergyRetentionFactor)
-
Fallbacks: If an actor doesn't implement
IBulletForgeInterface, the system uses globalBulletForgeDeveloperSettingsfor density (250 kg/m³), thickness (10 cm), and hardness. -
Interface Control: Implement
IBulletForgeInterfaceto:-
ForcePenetrationorForceRicochet. - Provide dynamic
MaterialThicknessandMaterialHardness. - Custom
PenetrationResistancecalculations.
-
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.
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;
}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:
-
Variables: Add
ArmorHealth(Float),MaxArmorHealth(Float), andResistanceIntensity(Float) to your Blueprint. -
Override: In the Interfaces section, double-click
Get Penetration Resistance. -
Graph:
- Divide
ArmorHealthbyMaxArmorHealth. - Pass the result into a Power node (Base), with
ResistanceIntensityas the Exp. - Multiply the original
Thickness * Hardness * Densitycalculation by the result of the Power node. - Return the final value.
- Divide
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:
-
Variables: Add
AbsorptionRate(Float, default 5000.0),ArmorHealth(Float), andMaxArmorHealth(Float). -
Override:
Get Penetration Resistance. -
Logic:
- Calculate
ImpactAreausing Calculate Impact Area node (passBulletDiameter). - 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.
- Calculate
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):
- Implement
IBulletForgeInterfacein your Blueprint. - Override
GetPenetrationPowerModifier. - Logic:
If (Trace.Settings.Tags.Contains("Incendiary")) return 500.0f; else return 0.0f;
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.
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:
- Implement
IBulletForgeInterface. - Override
Get Penetration Depth. - Logic: Multiply
Bullet Settings -> Base Damageby a scale factor, use Min withDefault Penetration Depth, and return.
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.
-
EnergyLossFactoris 1.0. - The bullet exits with 600J.
- A target behind the plate receives a
HitEventwithCurrentEnergy = 600J. If damage is scaled by energy, the lethality is effectively reduced by 40%.
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.
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 aFHitEventstruct containing theCurrentEnergyof the bullet at the moment of impact and theEnergyLossresulting 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:
- Attach
UBulletForgeReactiveComponentto your Actor (e.g., Character). - On the Server, bind a function to
OnHitEvent. - In the bound function, use
UBulletForgeFunctionLibrary::CalculateDamageFromHitEventto get the physics-scaled damage. - Call
UGameplayStatics::ApplyPointDamageor your custom damage function.
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
OnHitEventorOnPenetrationEventand useEnergyLoss(inFPenetrationEvent) or a calculation based onCurrentEnergyto reduce armor durability. -
Option B: Interface Overrides (For Dynamic Calculations):
If your armor's effectiveness changes based on its current durability, override
GetPenetrationResistanceinIBulletForgeInterface. 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.