ButterNet Movement Sync

Installation & Setup

Requirements

Unreal Engine 5.8. Server-authoritative networking - dedicated or listen. Compatible with Iris replication on or off; the batched mode registers its container as an Iris-aware fast array where Iris is enabled.

Install the plugin

  1. Install from Fab

    Add the plugin to your library and install it to your engine version from the Epic Games Launcher.

  2. Enable the plugin

    Edit → Plugins → Networking → ButterNet Movement Sync, then restart the editor.

  3. Add the component

    On the pawn Blueprint you want replicated: Add Component → ButterNet Movement Sync. No graph wiring is required - the component drives itself from BeginPlay.

  4. Disable Replicate Movement

    On the same actor, clear Replication → Replicate Movement. Two systems writing proxy transforms will fight every frame.

Why step 4 is mandatory

Unreal's native movement replication snaps a simulated proxy to each newly received FRepMovement. This component interpolates between the same poses. Leave both active and the proxy alternates between the two every frame, which reads as a persistent stutter. The component detects this and logs a warning naming the offending actor - see CompetingWriterTolerance.

Optional: enabling Iris

Iris is Unreal's newer replication backend. The plugin works with it on or off, and nothing needs changing on the component either way. Enabling it changes how the engine handles the state this plugin sends, and two of those changes matter enough to be worth understanding before you decide.

  1. Enable Iris and push model

    Add these to [SystemSettings] in Config/DefaultEngine.ini:

    net.Iris.UseIrisReplication=1
    net.Iris.PushModelMode=1
    net.IsPushModelEnabled=1
    net.SubObjects.DefaultUseSubObjectReplicationList=1
  2. Leave relevancy alone

    Pawns pick up Iris spatial filtering automatically, provided bAlwaysRelevant, bOnlyRelevantToOwner and bNetUseOwnerRelevancy are all false. You only need an explicit entry under [/Script/IrisCore.ObjectReplicationBridgeConfig] for a class that sets one of those and still wants spatial filtering.

  3. Rebuild

    These are engine-init settings, not runtime toggles. A running server will not pick them up.

What it changes for this plugin

Push model is the one that matters. Without it, a replicated property is polled and compared against a shadow copy on every update cycle, whether or not it changed. The component's dead-bands stop it sending when a pawn is still, but the engine still pays to check. With push model on, an unchanged pose is never marked dirty, so it is not compared, not serialised and not considered at all. That is what turns "this pawn did not move" into genuinely zero work rather than merely zero bytes.

Batched mode gets delta serialisation. The shared per-region container is a fast array, so a single pawn moving sends one item rather than the whole region's contents. This is the difference between batched mode scaling with the number of pawns that moved and scaling with the number of pawns present.

Iris on against Iris off has not been benchmarked for this plugin, so there is no speed figure to quote here. Both effects above are structural rather than incidental, but if the number matters to your decision, measure it on your own content.

Replication model

The component behaves differently per network role, and registers no work where it has none to do.

RoleBehaviour
AuthoritySamples the owner's transform on a timer at SendRate. No tick function is registered on a dedicated server at all.
SimulatedProxyTicks only to interpolate. Disables the pawn's movement component so it cannot integrate against the interpolated transform.
AutonomousProxyNothing. The owning client drives its own pawn; the replicated state is conditioned COND_SkipOwner so it never reaches them.

The wire format is a single replicated struct carrying position, quantised yaw/pitch/roll, velocity, a server timestamp and a teleport counter. Disabled channels are written as zero, which compresses to almost nothing rather than carrying stale data.

Replication is push-model: the property is only considered after an explicit dirty mark, which happens only when the sampled transform clears the dead-bands. A pawn standing still is not compared, not serialised and not sent. Because the payload is state rather than an event, a dropped update is self-correcting - the next one carries current truth - and a late-joining client receives the current pose as part of normal initial replication.

SampleAndSend()          // authority, timer-driven at SendRate
  └─ ShouldSend()        // dead-band test; early-out costs nothing
     └─ mark dirty       // push model: no mark, no consideration
        └─ per-actor  → replicated property, owner-skipped
           batched    → one item in the region's fast array

In batched mode a pawn registers with the container for its region and with neighbouring regions while inside the overlap band, so crossing a boundary never interrupts its update stream. Clients route arriving items by pawn reference into that pawn's own interpolation buffer, so a pose arriving from a different region continues the same playback without a reset. Duplicate poses produced by the overlap are rejected by timestamp.

Parameters

All values are per-component, editable in the Details panel. Distances are Unreal units, times are seconds.

PropertyTypeDefaultEffect
Bandwidth
SendRatefloat10Samples per second on the authority. Drives the sample timer; the dominant bandwidth term. Raise with InterpolationDelay.
PositionDeadbandfloat1.0Minimum translation before a sample is sent. Below this, nothing is marked dirty.
RotationDeadbandfloat1.0Minimum rotation in degrees, tested per enabled axis.
bSyncPositionbooltrueInclude location. Disable for fixed-position actors such as turrets.
bSyncYawbooltrueInclude yaw.
bSyncPitchboolfalseInclude pitch. Off by default; most walking pawns do not need it replicated.
bSyncRollboolfalseInclude roll. Enable for aircraft, ships and physics-driven pawns.
bSyncVelocitybooltrueInclude velocity so proxy animation blendspaces reading GetVelocity() stay correct with the movement component disabled.
bStaggerSendPhasebooltrueOffsets each component's timer phase so a large population does not sample on the same frame.
Batched mode
bUseBatchedReplicationbooltruePublish through the shared per-region container instead of replicating a property per pawn. Read at BeginPlay.
HubCellSizefloat10000Region edge length. Smaller regions tighten relevancy but create more containers, each of which is itself a replicated object. Must be identical across all pawns in a world.
HubOverlapMarginfloat1000Distance from a region edge at which a pawn also publishes into the adjacent region. Keeps handoff seamless at the cost of duplicate publication inside the band. Release uses 1.5× this value as hysteresis.
HubCullPaddingfloat10000Added to each region's cull radius beyond its half-diagonal. Must cover your pawn net cull distance plus vertical relief, since the grid is two-dimensional.
Smoothing
InterpolationDelayfloat0.25Playback offset behind the newest received state. Must exceed the inter-packet gap plus jitter. The primary jitter control.
MaxExtrapolationfloat0.25Maximum time the client extrapolates past the newest state before holding position.
SnapDistancefloat1000Distance beyond which an incoming pose is applied as a teleport rather than interpolated.
ClockCorrectionRatefloat0.1Rate at which the client playback clock converges on the server timeline.
BufferSizeint3220Retained state count. Must span InterpolationDelay at SendRate with headroom for a dropped packet.
Diagnostics
CompetingWriterTolerancefloat25Drift in units between where this component placed the pawn and where it is found next frame, above which a warning is logged once.

Blueprint API

FunctionReturnsNotes
ForceSyncvoidImmediate sample and send, bypassing both dead-bands. Call after any discontinuous move that is not a teleport.
ForceTeleportvoidImmediate send with the teleport counter incremented, so proxies snap rather than interpolate. Use for teleports and respawns.
SetSendRatevoidRestarts the sample timer at a new rate. Safe to drive from a significance or distance system.
SetSyncEnabledvoidClears the sample timer without destroying the component. Cheaper than gating inside the sample.
GetPlaybackLagfloatSeconds between the newest received state and the currently displayed pose.
GetBufferedStateCountint32Retained state count. Persistently 0 or 1 means the client is extrapolating.

Tuning

Start from InterpolationDelay ≈ 2 / SendRate. At the default 10 Hz that is the shipped 0.25 s, which tolerates one dropped packet without the buffer running dry.

Graph GetBufferedStateCount during play. A healthy client holds two or more states; sitting at 0 or 1 means playback has caught up with arrivals and is extrapolating, which is the usual root cause of reported jitter. Either raise InterpolationDelay or raise SendRate.

Dead-bands are the cheapest bandwidth lever and cost nothing at rest, but they quantise motion - a pawn moving slower than PositionDeadband × SendRate per second will visibly step. Lower the dead-band rather than raising the send rate for slow-moving pawns.

In batched mode, region size is a trade rather than an optimisation: smaller regions send each client less but increase the number of replicated containers the server tracks. If your container count approaches your pawn count, the region size is too small for how your population is distributed.

Troubleshooting

Proxies stutter or vibrate

Confirm Replicate Movement is disabled on the actor. If it is, check GetBufferedStateCount - a value of 0 or 1 means InterpolationDelay is too low for the current SendRate.

Proxies lag behind the server position

Expected and configured: InterpolationDelay is a deliberate offset. Reduce it and raise SendRate so the buffer still holds two states at the shorter delay.

A teleport animates as a fast slide

Call ForceTeleport rather than letting the next scheduled sample carry the new position, or lower SnapDistance below the teleport distance.

Competing movement writer warning in the log

Something else is writing the proxy transform. In order of likelihood: Replicate Movement still enabled, a movement component still ticking on proxies, or gameplay code calling SetActorLocation on a simulated proxy.

Distant pawns stop updating in batched mode

HubCullPadding is smaller than your pawn net cull distance, or your level's vertical extent exceeds it. Raise it, or use per-actor mode where relevancy is resolved per pawn.

Pawns never appear on clients

Check the owning actor replicates at all and that its net cull distance reaches the observing client. The component replicates pose, not existence - actor relevancy is still Unreal's.

Out of scope

The component replicates a pose and nothing else. Everything below is deliberately left to the engine or to your own systems, and knowing which is which will save time later.

Not handledWhat owns it instead
Actor relevancyUnreal. The component replicates pose, not existence. A pawn outside a client's net cull distance never appears, regardless of this component.
The owning client's own pawnYour movement code. State is conditioned COND_SkipOwner, so an autonomous proxy is never sent its own position.
Client-side prediction and reconciliationCharacterMovementComponent. This is not a replacement for it on player characters, and it does not roll back or replay moves.
Movement mode, crouch, jump and fall stateYour animation and movement systems. The wire format carries position, rotation and velocity only.
Root motion and montage stateUnreal's animation replication. Nothing here syncs montages.
Collision on proxiesNothing. Poses are applied without a sweep, so a simulated proxy will pass through geometry rather than resolve against it. This is intentional; the authority already resolved the movement.
Physics simulationChaos. Simulated bodies are not synchronised by this component.

The short version: it makes pawns the player does not control move smoothly, cheaply and correctly. It does not decide what those pawns are doing, and it does not attempt to replace character movement.

Support

Questions, bug reports and integration help - use the contact form on the main site, or the Fab product Q&A.

← Back to overview