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.
Platforms: Windows and Linux, both built and tested. The code is plain C++ against engine APIs with no platform-specific calls, so other platforms are likely to build, but they are not claimed here until they have been.
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. - Press play
That is the whole setup. If the actor had
Replicate Movementon, the component turns it off for you on the server and says so in the log - see below for why.
Why Replicate Movement is turned off for you
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.
Rather than make that a step you have to remember, the component clears the flag on the server at BeginPlay and logs that it did. Set bAutoDisableReplicateMovement to false if you deliberately want native replication as well. Anything else that moves the pawn behind the component's back is still detected and logged by name - 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 | Sampled at SendRate by a single world subsystem that walks every synced component. There is no timer and no tick function per pawn, and the component itself never ticks on a dedicated server. |
| SimulatedProxy | Ticks only to interpolate. Disables the pawn's movement component so it cannot integrate against the interpolated transform. |
| AutonomousProxy | In Server authority, nothing: the owning client drives its own pawn, and the replicated pose is conditioned COND_SimulatedOnly so it never reaches them. In Owner authority this is the sampler - it submits poses to the server, which validates and relays 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() // driven by the subsystem tick at SendRate
└─ ShouldSend() // dead-band test; early-out costs nothing
└─ mark dirty // push model: no mark, no consideration
└─ per-actor → replicated property, simulated-only
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 |
|---|---|---|---|
| Authority | |||
| Authority | enum | Server | Who samples the pawn. Server: the server samples and every client plays back. Owner: the owning client samples and submits, and the server validates before relaying. |
| OwnerMaxSpeed | float | 0 | Owner mode only. Speed limit in cm/s used to validate a submitted pose. 0 accepts anything the client sends - set it unless the client is trusted. |
| OwnerSpeedTolerance | float | 1.25 | Multiplier applied to OwnerMaxSpeed before rejecting, to absorb legitimate variation. |
| OwnerTimeout | float | 1.0 | Seconds of owner silence after which the server resumes sampling the pawn itself, so an unpossessed or disconnected owner never leaves it frozen. |
| Bandwidth | |||
| SendRate | float | 10 | Samples per second on the sampler. The dominant bandwidth term. Raise InterpolationDelay with it. |
| PositionDeadband | float | 1.0 | Minimum translation in units before a sample is sent. Below this, nothing is marked dirty. |
| RotationDeadband | float | 1.0 | Minimum rotation in degrees, tested per enabled axis. |
| VelocityDeadband | float | 10 | Minimum change in cm/s before velocity alone justifies a send. A stop is always reported. |
| ScaleDeadband | float | 0.01 | Minimum change before scale alone justifies a send. |
| bSyncPositionX | bool | true | Replicate position on X. |
| bSyncPositionY | bool | true | Replicate position on Y. |
| bSyncPositionZ | bool | true | Replicate position on Z. Turn off for a ground pawn that never leaves the floor: fewer bits, and a height wobble can no longer wake the dead-band. All three off makes it a rotation-only sync. |
| bSyncYaw | bool | true | Replicate yaw. |
| bSyncPitch | bool | false | Replicate pitch. Off by default; upright pawns never change it. On for flyers, swimmers, vehicles on uneven ground. |
| bSyncRoll | bool | false | Replicate roll. On for banking flyers, capsizing boats, anything aligning to a surface normal. |
| bSyncScaleX | bool | false | Replicate scale on X, to two decimal places. |
| bSyncScaleY | bool | false | Replicate scale on Y. |
| bSyncScaleZ | bool | false | Replicate scale on Z. All three off by default, so scale costs nothing unless asked for. |
| bSyncVelocity | bool | true | Replicate velocity, so proxy animation reading GetVelocity() stays correct with the movement component disabled. Also required for extrapolation. |
| bStaggerSendPhase | bool | true | Offsets each component's sample phase so a population spawned together does not sample on the same frame for life. |
| bSuspendWhileAttached | bool | true | Stop sampling while the actor is attached to another, and stop applying poses on receivers whose copy is attached. Resumes with an immediate send on detach. |
| Cost when nobody is looking | |||
| bUseDistanceTiers | bool | false | Opt in. Scales send rate down with distance to the nearest viewer and stops sampling entirely past the last band. |
| bUseDormancyWhenIdle | bool | false | Opt in. A pawn still for long enough drops out of replication and wakes on its next real movement. Takes the whole actor with it. |
| IdleSecondsBeforeDormant | float | 5.0 | How long a pawn must be still before dormancy is requested. |
| Batched mode | |||
| bUseBatchedReplication | bool | false | Off by default. Publish through a shared per-region container instead of a property per pawn. Read at BeginPlay. Built for tightly clustered populations; measured slower than per-actor on a scattered one. |
| HubCellSize | float | 10000 | Region edge length. Smaller regions tighten relevancy but create more containers, each 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. Release uses 1.5× this value as hysteresis. |
| HubCullPadding | float | 5000 | 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 a client carries a pawn forward past the newest state. |
| MaxExtrapolationDistance | float | 300 | Maximum distance in units a client may carry a pawn forward, whichever limit is reached first. |
| ExtrapolationRecoveryRate | float | 5.0 | How quickly the pawn eases back onto real data once it arrives, instead of snapping. |
| bExtrapolateRotation | bool | true | Carry facing forward from the last two samples while extrapolating, so a turning pawn does not freeze its heading. |
| SnapDistance | float | 1000 | Distance beyond which an incoming pose is applied as a teleport rather than interpolated. |
| ClockCorrectionRate | float | 1.0 | Exponential rate at which the client playback clock converges on the server timeline. Higher converges faster. |
| ClockSnapThreshold | float | 0.5 | Clock error in seconds beyond which the playback clock jumps rather than converges. |
| BufferSize | int32 | 20 | Retained state count. Must span InterpolationDelay at SendRate with headroom for a dropped packet. |
| Setup | |||
| bAutoDisableReplicateMovement | bool | true | On the server, turns the actor's own Replicate Movement off at BeginPlay and logs that it did. Set false only if you deliberately want native movement replication as well. |
| 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 |
|---|---|---|
| Control | ||
| ForceSync | void | Sample and send immediately, ignoring every dead-band. On the server in Owner mode this also corrects the owner to the server's pose. |
| ForceTeleport | void | Send immediately with the teleport counter incremented, so receivers snap rather than slide. Use for teleports and respawns. |
| SetSendRate | void | Change the base send rate at runtime. Clamps to 1 - 30. Applies immediately whether or not distance tiers are on. |
| SetSyncEnabled | void | Stop sampling and sending without destroying the component. Hub slots are kept, so re-enabling costs nothing. |
| SetPositionAxes | void | Set all three position switches at once. Prefer this over writing the bools individually during play: enabling an axis publishes straight away, where a bare write leaves receivers on a stale component until the dead-band next breaks. |
| SetScaleAxes | void | The same for the three scale switches. |
| SetComponentToSync | void | Sync a specific scene component instead of the whole actor, for a turret on a hull. Not replicated - call it on every machine. Null returns to the actor. |
| Diagnostics | ||
| GetComponentToSync | USceneComponent* | The scene component being synced, or null for the actor. |
| GetPlaybackLag | float | Seconds between the newest pose received and the one being shown. 0 before anything arrives. |
| GetPlaybackTime | float | The receiver's playback clock in server seconds. Compare against a server timestamp your own gameplay sent to line an event up with the pose being shown. |
| GetNewestServerTime | float | Server time of the newest pose received. 0 before the first. |
| GetBufferedStateCount | int32 | Retained pose count. Persistently 0 or 1 means InterpolationDelay is too low for the current SendRate and the receiver is extrapolating. |
| IsExtrapolating | bool | True while the receiver is past its newest pose and guessing. |
| GetEffectiveSendRate | float | The send rate currently in force, after distance tiers. 0 means paused. |
| IsOwnerDriving | bool | Owner mode: true while the owner's submissions are arriving and the server is relaying rather than sampling. |
| Events | ||
| OnTeleported | event | Fired on a receiver when an incoming pose was applied as a snap rather than a slide. |
| OnOwnerPoseRejected | event | Owner mode: fired on the owning client when the server refused a submitted pose and corrected it. |
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_SimulatedOnly, 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.