ButterNet Movement Sync
Installation & Setup
- Requirements
- Install the plugin
- Enabling Iris
- Replication model
- Parameters
- Blueprint API
- Tuning
- Troubleshooting
- Out of scope
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
- Install from Fab
Add the plugin to your library and install it to your engine version from the Epic Games Launcher.
- Enable the plugin
Edit → Plugins → Networking → ButterNet Movement Sync, then restart the editor. - Add the component
On the pawn Blueprint you want replicated:
Add Component → ButterNet Movement Sync. No graph wiring is required - the component drives itself fromBeginPlay. - 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.
- Enable Iris and push model
Add these to
[SystemSettings]inConfig/DefaultEngine.ini:net.Iris.UseIrisReplication=1 net.Iris.PushModelMode=1 net.IsPushModelEnabled=1 net.SubObjects.DefaultUseSubObjectReplicationList=1
- Leave relevancy alone
Pawns pick up Iris spatial filtering automatically, provided
bAlwaysRelevant,bOnlyRelevantToOwnerandbNetUseOwnerRelevancyare 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. - 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.
| Role | Behaviour |
|---|---|
| Authority | Samples the owner's transform on a timer at SendRate. No tick function is registered on a dedicated server at all. |
| SimulatedProxy | Ticks only to interpolate. Disables the pawn's movement component so it cannot integrate against the interpolated transform. |
| AutonomousProxy | Nothing. 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.
| Property | Type | Default | Effect |
|---|---|---|---|
| Bandwidth | |||
| SendRate | float | 10 | Samples per second on the authority. Drives the sample timer; the dominant bandwidth term. Raise with InterpolationDelay. |
| PositionDeadband | float | 1.0 | Minimum translation before a sample is sent. Below this, nothing is marked dirty. |
| RotationDeadband | float | 1.0 | Minimum rotation in degrees, tested per enabled axis. |
| bSyncPosition | bool | true | Include location. Disable for fixed-position actors such as turrets. |
| bSyncYaw | bool | true | Include yaw. |
| bSyncPitch | bool | false | Include pitch. Off by default; most walking pawns do not need it replicated. |
| bSyncRoll | bool | false | Include roll. Enable for aircraft, ships and physics-driven pawns. |
| bSyncVelocity | bool | true | Include velocity so proxy animation blendspaces reading GetVelocity() stay correct with the movement component disabled. |
| bStaggerSendPhase | bool | true | Offsets each component's timer phase so a large population does not sample on the same frame. |
| Batched mode | |||
| bUseBatchedReplication | bool | true | Publish through the shared per-region container instead of replicating a property per pawn. Read at BeginPlay. |
| HubCellSize | float | 10000 | Region 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. |
| HubOverlapMargin | float | 1000 | Distance 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. |
| HubCullPadding | float | 10000 | Added 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 | |||
| InterpolationDelay | float | 0.25 | Playback offset behind the newest received state. Must exceed the inter-packet gap plus jitter. The primary jitter control. |
| MaxExtrapolation | float | 0.25 | Maximum time the client extrapolates past the newest state before holding position. |
| SnapDistance | float | 1000 | Distance beyond which an incoming pose is applied as a teleport rather than interpolated. |
| ClockCorrectionRate | float | 0.1 | Rate at which the client playback clock converges on the server timeline. |
| BufferSize | int32 | 20 | Retained state count. Must span InterpolationDelay at SendRate with headroom for a dropped packet. |
| Diagnostics | |||
| CompetingWriterTolerance | float | 25 | Drift 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
| Function | Returns | Notes |
|---|---|---|
| ForceSync | void | Immediate sample and send, bypassing both dead-bands. Call after any discontinuous move that is not a teleport. |
| ForceTeleport | void | Immediate send with the teleport counter incremented, so proxies snap rather than interpolate. Use for teleports and respawns. |
| SetSendRate | void | Restarts the sample timer at a new rate. Safe to drive from a significance or distance system. |
| SetSyncEnabled | void | Clears the sample timer without destroying the component. Cheaper than gating inside the sample. |
| GetPlaybackLag | float | Seconds between the newest received state and the currently displayed pose. |
| GetBufferedStateCount | int32 | Retained 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 handled | What owns it instead |
|---|---|
| Actor relevancy | Unreal. 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 pawn | Your movement code. State is conditioned COND_SkipOwner, so an autonomous proxy is never sent its own position. |
| Client-side prediction and reconciliation | CharacterMovementComponent. 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 state | Your animation and movement systems. The wire format carries position, rotation and velocity only. |
| Root motion and montage state | Unreal's animation replication. Nothing here syncs montages. |
| Collision on proxies | Nothing. 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 simulation | Chaos. 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.
Questions, bug reports and integration help - use the contact form on the main site, or the Fab product Q&A.