新增FishNet文档

This commit is contained in:
2026-09-20 15:53:11 +08:00
parent c403145db9
commit 179c282405
431 changed files with 15476 additions and 0 deletions
@@ -0,0 +1,8 @@
---
description: >-
Prediction is the act of server-authoritative actions while allowing clients
to move in real-time without delay.
---
# Prediction
@@ -0,0 +1,41 @@
---
description: >-
Setting up the NetworkObject inspector for prediction is mandatory for using
prediction methods in scripts belonging to the NetworkObject.
---
# Configuring NetworkObject
All of the current options for the NetworkObject inspector are shown below. There are many but many will not be seen depending on your setup, and most are just there to provide you more fine-tuning for your game or setup.
<figure><img src="../../../.gitbook/assets/prediction-configuring-networkobject.png" alt=""><figcaption></figcaption></figure>
To begin you must first choose to Enable Prediction. Next you will set the Prediction Type. If you are using a rigidbody or rigidbody2D set the prediction type accordingly. Other is used for non-physics such as character controllers.
## State forwarding
State forwarding will allow the same inputs to run on all clients as they do on the server. This can be useful if you want all clients and server to run the same input based logic, similar to if the client or server owns the object. State forwarding is more CPU intensive as it means a state buffer must be kept, and the object must reconcile to make corrections as well re-run past states.
Even with the overhead state forwarding is often the most preferred approach because of the fact you do gain the ability to run code identical to how the client and server ran it, resulting in a more reliable simulation and potentially easier coding approach.
When state forwarding is disabled only the owner and server will run the inputs, and only the owner will keep the buffer for objects they own. This also means you must forward any information to clients that is essential to them displaying actions on non-owned objects, such as using a NetworkAnimator to now relay animations, or using RPCs to send gun fire audio. Movement is also not forwarded, so you may want to attach a NetworkTransform and specify it within the NetworkTransform field. Specifying the NetworkTransform will simply try to configure it to prediction based on your other NetworkObject inspector settings. If you are finding this is causing issues you can simply leave the field empty.
## Graphical object
This is the object which holds your graphics for the NetworkObject. By graphics, this means anything which holds visual representation that you would likely want smoothed between ticks, as well corrections from any potential de-synchronizations. It's very much worth mentioning you can leave this field empty and you will lose view additional options, such as Smoothing.
When using a graphical object it's very important to remember that you do not want any components on or beneath it that could negatively be affected by smoothing, such as a capsule to move your player. During a network tick prediction methods will run, and the TimeManager.TickDelta is used as the frame delta. Think of this as the same as if you were creating a client-authoritative physics controller while using FixedUpdate to move. Even as client-authoratitive, if you were to smooth the root the distance traveled during the FixedUpdate then your root would be in a different spot than where the physics engine left it. In result the movement and collision system wouldn't behave quite right. With all of that said, typically your colliders and triggers which affect gameplay or transforming the NetworkObject should be on the same GameObject as your NetworkObject. Or at the very least, not within the graphical object.
To understand more what this does read the component information [here](../../../fishnet-building-blocks/components/network-object.md). Detaching the graphical object is a supplemental feature which might be useful depending on your setup. The default is keeping the graphical a child, where it is. When this is true the graphical object rolls back to it's transform properties after the tick, then smooths to the transform properties it was after the tick. This is done so you can run lower tick rates and have smooth transform updates rather than everything stepping to whatever your tick rate delta is.
However, keeping the graphical object attached could be problematic for certain animation setups, or even cameras. Third party assets often do not expect transforms to teleport back and then be smooth to their destination over time, and in result you might see some funky outcomes of this action. Cameras as well can display what appears to be stuttering or jitter of your graphical object, even though it's completely fine. By detaching the graphical object it is no longer rolled back after the tick, and it simply exists in world space moving towards the proper goal over the duration of a tick. In most scenarios detaching would likely be better but there is always a chance it's not right for you, hence the default of staying attached.
## Smoothing
The smoothing options are only present when a graphical object is set. Most of these settings are explained well on the same NetworkObject component information page linked above.
There are certain cases where you may want to customize smoothing to control what is actually smoothed, versus what you want to control yourself. That's where the smoothed properties come in. You will notice there is a smoothed properties for if you are the owner of the object, or a spectator(not owner). In a number of cases we've seen developers wanting to self smooth things such as rotation while letting FishNet handle the position and scale. In such a scenario you would just untick rotation from the smoothed properties.
Changing the smoothing has absolutely no affect on the prediction itself, at least it shouldn't assuming you utilized the previous notes of graphical object.
Adaptive Interpolation under Spectator options determines the level of interpolation on spectated objects. Setting this to lower values ultimately means less interpolation, and in result a larger chance that a de-synchronization will be more apparent from a visual stand point. A higher value of course means more interpolation, and much less chance of seeing the effects of a de-synchronization. Whichever you use for this keep your game type in mind, or just play around with the settings under some latency until you see what you like. The adaptive interpolation API is also exposed letting you tweak it at runtime, and we have components that do just this to help you get that perfect feel for your game.
@@ -0,0 +1,31 @@
---
description: >-
The PredictionManager is responsible for global prediction settings, and other
prediction related information.
---
# Configuring PredictionManager
You do not necessarily even need to add it to your NetworkManager object, but doing so does offer you to alter default settings.
<figure><img src="../../../.gitbook/assets/prediction-manager-component.png" alt=""><figcaption></figcaption></figure>
## Interpolation
Depending on your game type you may want to adjust the default client and server interpolation. The tooltips provide helpful information, but in short more interpolation means more resilience against network instability at the cost of larger delays between running actions.
Having more interpolation also means reducing the chances of data having to predict data when it is expected to be known. There's a variety of ways to know if data is confirmed, predicted, or in the future; this topic is covered later.
Client interpolation indicates how many ticks inputs from the server(and other clients) are held before they are run.
For casual games an interpolation of 2-3 may be desired to drastically improve the likeliness inputs will always be available to run. This will of course add on a delay to when those inputs are run, so perhaps for fast paced games a value of 1 would be better.
Server interpolation is much the same but typically should be a lower value. This is how much of a buffer the server tries to hold for inputs, resulting in the server not running them after a number of ticks equal to the specified interpolation.
## Excessive replicate dropping
Typically speaking the server will never have more than it's Server Interpolation +/- 1 in queue. However, there is a chance if the client is having network issues and is sending inputs in burst the queue could for example go from 0 to 5, if 5 of clients inputs came through at once.
When the server queued inputs exceed the maximum it will begin to drop old values. This protects the server against an allocation attack and also prevents cheating by the client trying to send extra inputs.
You may disable dropping of excessive replicates but this opens your game up to cheating, as multiple replicates will be run per tick on the server to consume extras. There is still a generous hard-coded value of the maximum amount of replicates which may be queued up hidden to you; this is to protect from allocation attacks.
@@ -0,0 +1,13 @@
---
description: Very little of the TimeManager has to be configured for prediction.
---
# Configuring TimeManager
When using prediction it is essential that Fish-Networkings timing system is used. By default Unity's timing is used, but this can be changed on the TimeManager component.
Add the TimeManager component to your NetworkManager if it does not already exist.
<figure><img src="../../../.gitbook/assets/prediction-configuring-timemanager.png" alt=""><figcaption></figcaption></figure>
Once added change the Physics Mode to **Time Manager**, and you are done.
@@ -0,0 +1,8 @@
---
description: >-
This guide provides examples of creating prediction code, understanding
important aspects of the code, and leveraging it to your game.
---
# Creating Code
@@ -0,0 +1,198 @@
---
description: >-
This guide supplements the basic prediction guide by showing how to introduce
more complexities to your controls.
---
# Advanced Controls
{% hint style="info" %}
Be sure to review the previous guides in this section before reviewing this page.
{% endhint %}
## Guide goal
Implementing additional features into your prediction is much like you would code in a single player game, only remembering to reconcile anything that could de-synchronize the prediction.
In this guide a psuedo ground check before a jump will be added, as well a sprint function.
## Sprinting and ground checks
First the ReplicateData needs to be updated to contain our sprint action, which will rely on a stamina mechanic. Not much changed other than we added the Sprint boolean and set it using the constructor.
```csharp
public struct ReplicateData : IReplicateData
{
public bool Jump;
public bool Sprint;
public float Horizontal;
public float Vertical;
public ReplicateData(bool jump, bool sprint, float horizontal, float vertical) : this()
{
Jump = jump;
Sprint = sprint;
Horizontal = horizontal;
Vertical = vertical;
}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
```
After updating the ReplicateData we need to poll for the sprint key when creating the data, like you would in most games.
We are re-using methods from our previous guides so much of this should be familiar.
```csharp
private ReplicateData CreateReplicateData()
{
if (!base.IsOwner)
return default;
// Build the replicate data with all inputs which affect the prediction.
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
/* Sprint if left shift is held.
* You do not necessarily have to perform security checks here.
* For example, it was mentioned sprint will rely on stamina, we
* are not checking the stamina requirement here. You certainly could
* as a precaution but this is only building the replicate data, not where
* the data is actually executed, which is where we want
* the check. */
bool sprint = Input.GetKeyDown(KeyCode.LeftShift);
ReplicateData md = new ReplicateData(_jump, sprint, horizontal, vertical);
_jump = false;
return md;
}
```
Declare a stamina float in your class.
```csharp
// Current stamina for the player.
private float _stamina;
```
Now use our new Sprint bool and stamina field to apply sprinting within the replicate method.
```csharp
[Replicate]
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
float delta = (float)base.TimeManager.TickDelta;
// Regenerate stamina at 3f per second.
_stamina += (3f * delta);
// How much it cost to use sprint per delta.
// This causes sprint to use stamina twice as fast
// as the stamina recharges.
float sprintCost = (6f * delta);
Vector3 forces = new Vector3(data.Horizontal, 0f, data.Vertical) * _moveRate;
// If sprint is held and enough stamina exist then multiple forces.
if (data.Sprint && _stamina >= sprintCost)
{
// Reduce stamina by cost.
_stamina -= sprintCost;
// Increase forces by 30%.
forces *= 1.3f;
}
/* You should check for any changes in replicate like we do
* with stamina. Recall how it was said checking stamina when
* gathering the inputs is not so important, but doign so in the replicate
* is what grants server authority, as well makes prediction function
* properly with corrections and rollbacks. */
/* Now check if to jump. IsGrounded() does not exist, we're going to
* pretend it uses a raycast or overlap to check. */
if (data.Jump && IsGrounded())
{
Vector3 jmpFrc = (Vector3.up * _jumpForce);
PredictionRigidbody.AddForce(jmpFrc, ForceMode.Impulse);
}
// Rest of the code remains the same.
}
```
{% hint style="danger" %}
If a value can affect your prediction do not store it outside the replicate method, unless you are also reconciling the value. An exception applies if you are setting the value inside your replicate method.
This is a very important detail to remember, and is discussed further below.
{% endhint %}
Reconciling only a rigidbody state is very simple.
```csharp
[Reconcile]
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
{
// Call reconcile on your PredictionRigidbody field passing in
// values from data.
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
}
```
{% hint style="warning" %}
If you are using multiple rigidbodies you at the very least need to reconcile their states as well. You can do so quickly by adding a RigidbodyState for each rigidbody to your reconcile.
If you are also applying forces to these rigidbodies be sure to use PredictionRigidbody with them, and reconcile the PredictionRigidbody instead of RigidbodyState.
{% endhint %}
## Changes to reconcile
Because objects can reconcile to previous states it's fundamental to also reconcile any values stored outside the replicate method. Imagine if you had 10f stamina, enough to sprint, and did so successfully on the server and owner. After your sprint you only had 1f stamina, not enough to sprint further.
If you were to reconcile without resetting stamina to it's previous values then you would still be at 1f stamina after reconciling. Your replayed inputs, which previously allowed the sprint, would not sprint because you now lacked the needed stamina. In result of this, you would have a de-synchronization which would most likely be seen as jitter.
Including more variables in your prediction is fortunately easy enough. All you have to do is update your reconcile to include states or your new values or variables.
Added to our ReconcileData structure is a Stamina float.
```csharp
public struct ReconcileData : IReconcileData
{
public PredictionRigidbody PredictionRigidbody;
public float Stamina;
public ReconcileData(PredictionRigidbody pr, float stamina) : this()
{
PredictionRigidbody = pr;
Stamina = stamina;
}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
```
Then of course we must include the current value of stamina within our created reconcile data.
```csharp
public override void CreateReconcile()
{
ReconcileData rd = new ReconcileData(PredictionRigidbody, _stamina);
ReconcileState(rd);
}
```
Very last you must utilize the new reconcile data to reset the stamina state.
```csharp
[Reconcile]
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
{
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
_stamina = data.Stamina;
}
```
With minor additions to the code you now have an authoritative ground check as well a stamina driven sprint. Just like that, tic-tac-toe, a winner.
@@ -0,0 +1,245 @@
---
description: Learn how to create a predicted object that the owner or server can control.
---
# Controlling an Object
## Data structures
Implementing prediction is done by creating a replicate and reconcile method, and making calls to the methods accordingly.
Your replicate method will take inputs you wish to run on the owner, server, and other clients if using state forwarding. This would be any input needed for your controller such as jumping, sprinting, movement direction, and could even include other mechanics such as holding a fire button.
The reconcile method takes a state of the object after a replicate is performed. This state is used to make corrections in the chance of de-synchronizations. For example, you may send back health, velocity, transform position and rotation, so on.
It's also worth mentioning if you are going to allocate in your structures it could be beneficial to utilize the Dispose callback, which will run as the data is being discarded.
Here are the two structures containing basic mechanics for a rigidbody.
```csharp
public struct ReplicateData : IReplicateData
{
public bool Jump;
public float Horizontal;
public float Vertical;
public ReplicateData(bool jump, float horizontal, float vertical) : this()
{
Jump = jump;
Horizontal = horizontal;
Vertical = vertical;
}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
public struct ReconcileData : IReconcileData
{
// PredictionRigidbody is used to synchronize rigidbody states
// and forces. This could be done manually but the PredictionRigidbody
// type makes this process considerably easier. Velocities, kinematic state,
// transform properties, pending velocities and more are automatically
// handled with PredictionRigidbody.
public PredictionRigidbody PredictionRigidbody;
public ReconcileData(PredictionRigidbody pr) : this()
{
PredictionRigidbody = pr;
}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
```
{% hint style="info" %}
Learn more about using [PredictionRigidbody](../predictionrigidbody.md).
{% endhint %}
## Preparing to call prediction methods
Typically speaking you would want to run your replicate(or inputs) during OnTick. When you send the reconcile depends on if you are using physics bodies or not.
When using physics bodies, such as a rigidbody, you would send the reconcile during OnPostTick because you want to send the state after the physics have simulated your replicate inputs. See the [TimeManager API ](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Timing.TimeManager.html#FishNet_Managing_Timing_TimeManager_OnPostTick)for more details on tick and physics event callbacks.
Non-physics controllers can also send in OnTick, since they do not need to wait for a physics simulation to have the correct outcome after running inputs.
The code below shows which callbacks and API to use for a rigidbody setup.
{% hint style="info" %}
You may need to modify move and jump forces depending on the shape, drag, and mass of your rigidbody.
{% endhint %}
```csharp
// How much force to add to the rigidbody for jumps.
[SerializeField]
private float _jumpForce = 8f;
// How much force to add to the rigidbody for normal movements.
[SerializeField]
private float _moveForce = 15f;
// PredictionRigidbody is set within OnStart/StopNetwork to use our
// caching system. You could simply initialize a new instance in the field
// but for increased performance using the cache is demonstrated.
public PredictionRigidbody PredictionRigidbody;
// True if to jump next replicate.
private bool _jump;
private void Awake()
{
PredictionRigidbody = ObjectCaches<PredictionRigidbody>.Retrieve();
PredictionRigidbody.Initialize(GetComponent<Rigidbody>());
}
private void OnDestroy()
{
ObjectCaches<PredictionRigidbody>.StoreAndDefault(ref PredictionRigidbody);
}
public override void OnStartNetwork()
{
base.TimeManager.OnTick += TimeManager_OnTick;
base.TimeManager.OnPostTick += TimeManager_OnPostTick;
}
public override void OnStopNetwork()
{
base.TimeManager.OnTick -= TimeManager_OnTick;
base.TimeManager.OnPostTick -= TimeManager_OnPostTick;
}
```
{% hint style="info" %}
Instead of needing to manually subscribe to the TimeManager events such as OnTick and OnPostTick you can have your class inherit from [TickNetworkBehaviour](../../../../fishnet-building-blocks/components/network-behaviour-components/ticknetworkbehaviour.md) and then use its override methods.
{% endhint %}
## Calling prediction methods
For our described demo, below is how you would gather input for your replicate and reconcile methods.
Update is used to gather inputs which are only fired for a single frame. Ticks do not occur every frame, but rather at the interval of your TickDelta, much like FixedUpdate works. While the code below only uses Update for single frame inputs there is nothing stopping you from using it for held inputs as well.
```csharp
private void Update()
{
if (base.IsOwner)
{
if (Input.GetKeyDown(KeyCode.Space))
_jump = true;
}
}
```
OnTick will now be used to build our replicate data. A separate method of 'CreateReplicateData' is not needed to create the data but is done to organize our code better.
When attempting to create the replicate data we return with default if not the owner of the object. Server receives and runs inputs from the owner so it does not need to create data, and when clients do not own an object they will get the input for it from the server, as forwarded by other clients if using state forwarding. When not using state forwarding default should still be used in this scenario, but clients will not run replicates on non-owned objects. You can also run inputs on the server if there is no owner; using base.HasAuthority would probably be best for this. See [Checking Ownership](../../ownership/#checking-ownership) for more information.
```csharp
private void TimeManager_OnTick()
{
RunInputs(CreateReplicateData());
}
private ReplicateData CreateReplicateData()
{
if (!base.IsOwner)
return default;
// Build the replicate data with all inputs which affect the prediction.
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
ReplicateData md = new ReplicateData(_jump, horizontal, vertical);
_jump = false;
return md;
}
```
Now implement your replicate method. The name may be anything but the parameters shown are required. The first is what we pass in, the remainder are set at runtime. Although, you absolutely may change the default channel used in the parameter or even at runtime.
For example, it could be beneficial to send an input as reliable if you absolutely want to ensure it's not dropped due to network issues.
```csharp
[Replicate]
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
/* ReplicateState is set based on if the data is new, being replayed, ect.
* Visit the ReplicationState enum for more information on what each value
* indicates. At the end of this guide a more advanced use of state will
* be demonstrated. */
// Be sure to always apply and set velocities using PredictionRigidbody
// and never on the rigidbody itself; this includes if also accessing from
// another script.
Vector3 forces = new Vector3(data.Horizontal, 0f, data.Vertical) * _moveForce;
PredictionRigidbody.AddForce(forces);
if (data.Jump)
{
Vector3 jmpFrc = new Vector3(0f, _jumpForce, 0f);
PredictionRigidbody.AddForce(jmpFrc, ForceMode.Impulse);
}
// Add gravity to make the object fall faster. This is of course
// entirely optional.
PredictionRigidbody.AddForce(Physics.gravity * 3f);
// Simulate the added forces.
// Typically you call this at the end of your replicate. Calling
// Simulate is ultimately telling the PredictionRigidbody to iterate
// the forces we added above.
PredictionRigidbody.Simulate();
}
```
{% hint style="info" %}
On non-owned objects a number of replicates will arrive as ReplicateState Created, but will contain default values. This is our PredictionManager.RedundancyCount feature working.
This is normal and indicates that the client or server had gracefully stopped sending states as there is no new data to send. This can be useful if you are [Predicting States.](understanding-replicatestate/predicting-states-in-code.md)
{% endhint %}
Now the reconcile must be sent to clients to perform corrections. Only the server will actually send the reconcile but be sure to call CreateReconcile no matter if client, server, owner or not; this is to future proof an upcoming feature. Unlike our CreateReplicateData method, using CreateReconcile is not optional.
```csharp
private void TimeManager_OnPostTick()
{
CreateReconcile();
}
// Create the reconcile data here and call your reconcile method.
public override void CreateReconcile()
{
// We must send back the state of the rigidbody. Using your
// PredictionRigidbody field in the reconcile data is an easy
// way to accomplish this. More advanced states may require other
// values to be sent; this will be covered later on.
ReconcileData rd = new ReconcileData(PredictionRigidbody);
// Like with the replicate you could specify a channel here, though
// it's unlikely you ever would with a reconcile.
ReconcileState(rd);
}
```
Reconciling only a rigidbody state is very simple.
```csharp
[Reconcile]
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
{
// Call reconcile on your PredictionRigidbody field passing in
// values from data.
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
}
```
{% hint style="info" %}
When reconciling an object with a CharacterController, ensure you disable the CharacterController component immediately before setting its position and re-enable it afterwards. Because this controller uses a custom physics engine, skipping this toggle will prevent the internal state from refreshing, resulting in a synchronization error between the physics and the transform.\
As an example:<br>
```csharp
_characterController.enabled = false;
transform.localPosition = reconcileData.Position;
_characterController.enabled = true;
```
{% endhint %}
@@ -0,0 +1,110 @@
---
description: >-
A very simple script for keeping non-controlled objects in synchronization
with the prediction system.
---
# Non-Controlled Object
Many games will require physics bodies to be networked, even if not controlled by players or the server. These objects can also work along-side the new state system by adding a basic prediction script on them.
It's worth noting that you can also 'control' non-owned objects on the server by using base.HasAuthority. This was discussed previously [here](controlling-an-object.md).
## Sample script
Below is a full example script to synchronize a non-controlled rigidbody. Since the rigidbody is only reactive, input polling is not needed. Otherwise you'll find the data structures are near identical to the ones where we took input.
{% hint style="warning" %}
It is strongly recommended to review the [controlling objects ](controlling-an-object.md)guide for additional notes in understanding the code below.
{% endhint %}
```csharp
public class RigidbodySync : NetworkBehaviour
{
// Replicate structure.
public struct ReplicateData : IReplicateData
{
// The uint isn't used but Unity C# version does not
// allow parameter-less constructors we something
// must be set as a parameter.
public ReplicateData(uint unused) : this() {}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
// Reconcile structure.
public struct ReconcileData : IReconcileData
{
public PredictionRigidbody PredictionRigidbody;
public ReconcileData(PredictionRigidbody pr) : this()
{
PredictionRigidbody = pr;
}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
// Forces are not applied in this example but you
// could definitely still apply forces to the PredictionRigidbody
// even with no controller, such as if you wanted to bump it
// with a player.
private PredictionRigidbody PredictionRigidbody;
private void Awake()
{
PredictionRigidbody = ResettableObjectCaches<PredictionRigidbody>.Retrieve();
PredictionRigidbody.Initialize(GetComponent<Rigidbody>());
}
private void OnDestroy()
{
ResettableObjectCaches<PredictionRigidbody>.StoreAndDefault(ref PredictionRigidbody);
}
// In this example we do not need to use OnTick, only OnPostTick.
// Because input is not processed on this object you only
// need to pass in default for RunInputs, which can safely
// be done in OnPostTick.
public override void OnStartNetwork()
{
base.TimeManager.OnPostTick += TimeManager_OnPostTick;
}
public override void OnStopNetwork()
{
base.TimeManager.OnPostTick -= TimeManager_OnPostTick;
}
private void TimeManager_OnPostTick()
{
RunInputs(default);
CreateReconcile();
}
[Replicate]
private void RunInputs(ReplicateData md, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
// If this object is free-moving and uncontrolled then there is no logic.
// Just let physics do it's thing.
}
// Create the reconcile data here and call your reconcile method.
public override void CreateReconcile()
{
ReconcileData rd = new ReconcileData(PredictionRigidbody);
ReconcileState(rd);
}
[Reconcile]
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
{
// Call reconcile on your PredictionRigidbody field passing in
// values from data.
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
}
}
```
@@ -0,0 +1,58 @@
---
description: >-
Being familiar with what each state means will help you fine-tune your
gameplay on spectated objects.
---
# Understanding ReplicateState
{% hint style="info" %}
If you need a refresher on what each state means see our [API](https://fish-networking.com/FishNet/api/api/FishNet.Object.ReplicateState.html) or simply navigate to the ReplicateState enum in your code editor.
{% endhint %}
Each state is a flag value; a replicate state may contain multiple flags. There are numerous extensions you may use to check if a state contains certain values.
{% hint style="success" %}
You can view all built-in state extensions within the ReplicateState file.
{% endhint %}
## Invalid
An invalid ReplicateState should never occur. This would imply internally Fish-Networking failed to properly set the state.
## Ticked
Server and clients use this flag. This flag will be set if the data tick has run outside a reconcile, such as from user code within OnTick . Ticked can exist during a replay/reconcile, but only if the data had first run outside the replay/reconcile.
## Replayed
Only clients will use this flag. The replayed flag is set if data is being run during a reconcile.
{% hint style="info" %}
The server is always considered right and never has to correct data, so it never reconciles or replays inputs
{% endhint %}
## Created
Server and client use this flag. A created flag indicates that the data was created by controller, such as owner or the server if no owner. The created flag will not be present if the controller has not sent updates, such as if they are not taking any action.
## State examples
Below are examples of some possible states and what they mean.
```csharp
// You will see this value if the data is being replayed, it previously ran outside the
// reconcile, and data is created by controller.
state = (ReplicateState.Replayed | ReplicateState.Ticked | ReplicateState.Created);
// When the state is only Replayed then the data is not Created, and the tick on data
// has not occurred outside the reconcile yet. This is what we often refer to as a
// future state.
state = ReplicateState.Replayed;
// When a state is Ticked only it indicates that the data is being run outside a
// reconcile, and that the controller has not sent data for this particular tick.
state = ReplicateState.Ticked;
```
{% hint style="success" %}
For more possible flag examples see the state extensions (the ReplicateState file).
{% endhint %}
@@ -0,0 +1,110 @@
---
description: >-
Due to the unpredictability of the Internet inputs may drop or arrive late.
Predicting states is a simple way to compensate for these events.
---
# Predicting States in Code
If your game is fast-paced and reaction based, even if not using physics, predicting states can be useful to _predict_ the next inputs you'll get from the server before you actually get them.
{% hint style="info" %}
Both the server and client can predict inputs.
The server may predict inputs to accommodate for clients with unreliable connections.
Clients would predict inputs on objects they do not own, or as we often call them "spectated objects".
{% endhint %}
By predicting inputs you can place objects in the future of where you know them to be, and even make them entirely real-time with the client. Keep in mind however, when predicting the future, if you guess wrong there will be a de-synchronization which may be seen as jitter when it's corrected in a reconcile.
Below is an example of a simple replicate method.
```csharp
// What data does is irrelevant in this example.
// We're only interested in how to predict a future state.
[Replicate]
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
float delta = (float)base.TimeManager.TickDelta;
transform.position += new Vector3(data.Horizontal, 0f, data.Vertical) * _moveRate * delta;
}
```
Before we go any further you must first understand what each ReplicateState is. These change based on if input is known, replaying inputs or not, and more. You can check out the [ReplicateState API](https://fish-networking.com/FishNet/api/api/FishNet.Object.ReplicateState.html) which will explain thoroughly. You can also find this information right in the source of FishNet.
If you've read through ReplicateState and do not fully understand them please continue reading as they become more clear as this guide progresses. You can also visit us on Discord for questions!
Covered in the ReplicateStates API: CurrentCreated will only be seen on clients if they own the object. When inputs are received on spectated objects clients run them in the reconcile, which will have the state ReplayedCreated. Clients will also see ReplayedFuture and CurrentFuture on spectated objects.
A state ending in 'Future' essentially means input has not been received yet, and these are the states you could predict.
Let's assume your game has a likeliness that players will move in the same direction regularly enough. If a player was holding forward for three ticks the input would look like this...
```csharp
(data.Vertical == 1)
(data.Vertical == 1)
(data.Vertical == 1)
```
But what if one of the inputs didn't arrive, or arrived late? The chances of inputs not arriving at all are pretty slim, but arriving late due to network variance is extremely common. If perhaps an input did arrive late the values may appear as something of this sort...
```csharp
(data.Vertical == 1)
(data.Vertical == 1)
(data.Vertical == 0) // Didn't arrive here, but will arrive late next tick.
(data.Vertical == 1) // This was meant to arrive the tick before, but arrived late.
```
Because of this interruption the player may seem to move forward twice, pause, then forward again. Realistically to help cover this up you will have interpolation on your graphicalObject as shown under the prediction settings for [NetworkObject.](../../../../../fishnet-building-blocks/components/network-object.md) The [PredictionManager](../../../../../fishnet-building-blocks/components/prediction/) also offers QueuedInputs which can give you even more of a buffer. For the sake of this guide though we're going to pretend both of those didn't get the job done, and you need to account for the late input.
Below is a simple way to track and use known inputs to create predicted ones.
```csharp
private ReplicateData _lastCreatedInput = default;
[Replicate]
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
// If inputs are not known. You could predict
// all the way into CurrentFuture, which would be
// real-time with the client. Though the more you predict
// in the future the more you are likely to miss predict.
if (state.IsFuture())
{
uint lastCreatedTick = _lastCreatedInput.GetTick();
// If it's only been 2 ticks since the last created
// input then run the logic below.
// This essentially means if the last created tick
// was 100, this logic would run if the future tick was 102
// or less. This is an example of a basic approach to only
// predict a certain number of inputs.
uint thisTick = data.GetTick();
if ((data.GetTick() - lastCreatedTick) <= 2)
{
// We do not necessarily want to predict all states.
// For example, it probably wouldn't make sense to predict
// multiple jumps in a row. In this example only the movement
// inputs are predicted.
data.Vertical = _lastCreatedInput.Vertical;
}
}
// If created data then set as lastCreatedInput.
else if (state == ReplicateState.ReplayedCreated)
{
// If ReplicateData contains fields which could generate garbage you
// probably want to dispose of the lastCreatedInput
// before replacing it. This step is optional.
_lastCreatedInput.Dispose();
// Assign newest value as last.
_lastCreatedInput = data;
}
float delta = (float)base.TimeManager.TickDelta;
transform.position += new Vector3(data.Horizontal, 0f, data.Vertical) * _moveRate * delta;
}
```
{% hint style="warning" %}
If your ReplicateData allocates do not forget to dispose of the lastCreatedInput when the network is stopped.
{% endhint %}
@@ -0,0 +1,190 @@
---
description: >-
Understanding how to use states will greatly improve your experience when
writing code for your replicate method.
---
# Using States in Code
## Future states
You will see the term 'in the future' or 'future state' used frequently when working with prediction. When in the future it's not possible to know the data from the controller, which is why we call it the _future_. When using client-prediction, as a client you are always moving in real-time, before even knowing the servers current state. Due to this, you will not know other clients or server states until they are forwarded to you, and during that time of unknowing we consider the object to be in the future.
The future this is where you gain the opportunity to predict future input from the controller. Or, you can simply prevent future movement entirely; uses vary depending on your needs. We'll cover this more later on this page.
{% hint style="info" %}
You will never be in the future on objects you controller, given you only replay inputs up to what you created and never beyond.
{% endhint %}
## The created flag
When a state is not created the data will be default. This often catches a lot of developers off guard as they might expect to see continual input from the controller, such as if the controller is always holding a movement key.
A common use case is updating the objects animator only when data is known.
```csharp
[Replicate]
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
// Left/right movement.
float horizontal = data.Horizontal;
// Only update the animator if data is created. Do not update the animator
// if not created as this will cause the animator to switch between having input
// and a default value.
if (state.ContainsCreated())
_myAnimator.SetFloat("Horizontal", horizontal);
}
```
{% hint style="warning" %}
If you are using State Order -> Inserted on the PredictionManager then Created will only ever be set on spectated objects during a reconcile. Since states on spectated obejcts are inserted into the replicate history they will run during reconciles rather than outside of reconciles. See the [PredictionManager guide](../../../../../fishnet-building-blocks/components/managers/predictionmanager.md) for more information on State Orders.
{% endhint %}
## Ticked
As mentioned before Ticked indicates the data has run outside a reconcile. Also described, the state can be Ticked as well Replayed, which means it ran outside a reconcile previously but is currently running again during a replay/reconcile.
Ticked and not replayed can often be used to perform one-time actions, such as showing visual effects, such as jumping. You probably wouldn't want to play jump audio when the jump first occurs, as well every time the input replays during a reconcile.
```csharp
[Replicate]
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
if (data.Jump)
{
DoJump();
// If ticked and not replayed then also play jump audio.
if (state.ContainsTicked() && !state.ContainsReplayed())
PlayJumpAudio();
}
}
```
## Preventing future state logic and movement
The replayed state is most commonly used to predict the future, or limit as opposition, limit the future.
Limiting future velocities is where the replayed flag is typically used the most. This keeps the object out of future prediction, which can limit real-time reflection of the object, but also prevents excessive corrections or movement snapping.
Below is a pretty basic example showing jumping and moving, without going too into depth of move rates. On a spectated object without any future checks, during a replay the object will jump and then continue to snap upward as you replay into the future (beyond data you could possibly know due to latency).
{% hint style="success" %}
This behavior can be difficult to explain, but is easy to see. Try our character controller prediction demo with and without an IsFuture check.
{% endhint %}
```csharp
[Replicate]
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
// Exit the method early to prevent going into the future, which would
// result in the controller snapping upward very fast when replaying a jump.
if (state.IsFuture())
return;
// Set vertical velocity to jump up.
if (data.Jump)
_verticalVelocity = 10f;
// Only add vertical movement for this example.
// Realistically, you would have x and z movement as well.
Vector3 movement = new(0f, _verticalVelocity, 0f);
// Reduce vertical velocity to begin falling, but prevent it from going too low.
_verticalVelocity -= (float)base.TimeManager.TickDelta;
_verticalVelocity = Mathf.Max(_verticalVelocity, -5f);
_characterController.Move(movement);
}
```
The example above shows a very easy implementation of preventing future movement on a character controller, but rigidbodies are a little different. With rigidbodies, even if you exit the method early preventing additional added velocities, the rigidbody still carries existing velocities; this is because physics simulates with every tick, replayed or not, regardless of if replicate runs.
Here's an example **without** preventing future movement on a rigidbody.
{% hint style="info" %}
The code below shows use of[ PredictionRigidbody](../../predictionrigidbody.md). You do not need to understand how the component works to understand this example.
{% endhint %}
```csharp
[Replicate]
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
float horizontal = data.Horizontal;
Vector3 movement = new(horizontal, 0f, 0f);
_predictionRigidbody.AddVelocity(movement);
_predictionRigidbody.Simulate();
// Nothing in this code prevents the rigidbody from moving into the future.
}
```
Preventing future movement on rigidbodies is a few more lines of code, but still pretty easy. We're going to use the RigidbodyPauser reference on the NetworkObject to pause the rigidbody when in the future. The RigidbodyPauser is a lesser known part of our [NetworkObject API ](https://fish-networking.com/FishNet/api/api/FishNet.Object.NetworkObject.html#FishNet_Object_NetworkObject_RigidbodyPauser)and is used almost exclusively for prediction.
```csharp
[Replicate]
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
// Only the client would need to pause to prevent future movement,
// and only on objects they do not own (objects they are spectating).
bool canChangePause = !base.IsOwner && !base.IsServerStarted;
// If in the future do not process any logic and pause the rigidbody.
// If not in the future, then unpause the rigidbody.
// There are no negative side-effects of calling pause or unpause when
// when the RigidbodyPauser is already in the same state.
if (state.IsFuture()
{
// This will prevent the rigidbody from moving by
// making it kinematic. Pausing a rigidbody does also mean
// objects can potentially pass through it. Another approach
// will be shown in the next example.
if (canChangePause)
base.NetworkObject.RigidbodyPauser.Pause();
}
else
{
// When not future, unpause. This allows the object to move
// again. Unpausing will restore velocities as they were
// prior to pausing.
if (canChangePause)
base.NetworkObject.RigidbodyPauser.Unpause();
float horizontal = data.Horizontal;
Vector3 movement = new(horizontal, 0f, 0f);
_predictionRigidbody.AddVelocity(movement);
_predictionRigidbody.Simulate();
}
}
```
As explained in the example above pausing a rigidbody can cause objects to pass-through it; this is due to the pauser making the rigidbody kinematic.
Here's a different technique which instead simply zeros out velocities when in the future. In most cases this new code snippet is most ideal, but it's good to have an understanding that both are available.
```csharp
[Replicate]
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
// If in the future then simply zero out velocities
// and exit the method to prevent checking any input logic.
// Using this approach will allow objects to still collide with
// the rigidbody.
if (state.IsFuture())
{
_myPredictionRigidbody.Velocity(Vector3.Zero);
_myPredictionRigidbody.AngularVelocity(Vector3.Zero);
return;
}
float horizontal = data.Horizontal;
Vector3 movement = new(horizontal, 0f, 0f);
_predictionRigidbody.AddVelocity(movement);
_predictionRigidbody.Simulate();
}
```
@@ -0,0 +1,57 @@
---
description: >-
Fish-Networking generates comparers for prediction data to perform internal
optimizations, but on occasion certain types cannot have comparers
automatically generated.
---
# Custom Comparers
You may see an error in the console about a type requiring a custom comparer. For example, generics and arrays specifically must have a custom comparer provided.
```csharp
/* For example, this will create an error stating
* byte[] needs a custom comparer. */
public struct MoveData : IReplicateData
{
public Vector2 MoveDirection;
public byte[] CustomData;
// rest omitted..
}
```
While a comparer could be made automatically for the type byte\[], we still require you to create your own as we may not know exactly how you want to compare these types. The code below compares byte arrays by iterating every byte to check for mismatches. Given how often prediction data sends, this could potentially burden the processor.
```csharp
[CustomComparer]
public static bool CompareByteArray(byte[] a, byte[] b)
{
bool aNull = (a is null);
bool bNull = (b is null);
// Both are null.
if (aNull && bNull)
return true;
// One is null, other is not.
if (aNull != bNull)
return false;
// Not same lengths, cannot match.
if (a.Length != b.Length)
return false;
​
// Both not null and same length, compare bytes.
int length = a.Length;
for (int i = 0; i < length; i++)
{
// Differs.
if (a[i] != b[i])
return false;
}
​
// Fall through, if here everything matches.
return true;
}
```
The above code is a working example of how to create a custom comparer, but it may not be the most ideal comparer for your needs; this is why we require you to make your own comparer for such types.
Creating your own comparer is simple. Make a new static method with any name and boolean as the return type. Decorate the method with the **\[CustomComparer]** attribute. There must also be two parameters, each being the type you want to compare. The method logic can contain whichever code you like
@@ -0,0 +1,17 @@
---
description: >-
Both the PredictionManager and NetworkObject have interpolation values, but
with separate objectives
---
# Interpolations
PredictionManager interpolation is not the same as NetworkObject interpolation.
Interpolation on the PredictionManager holds a number of states in queue as interpolation while NetworkObject interpolation handles graphical smoothing after running states.
A prediction interpolation of 0, be it client or server setting, means that states would be run soon as they are received. Most games will not use 0 interpolation as it's generally best to have at least 1 state in queue to compensate for latency. For every 1 interpolation added is like adding latency to when spectated states are run, equal to (interpolation \* TickDelta).
For smoothing graphical objects beneath your NetworkObject see [NetworkTickSmoother](../../../fishnet-building-blocks/components/tick-smoothers/networkticksmoother.md). This component allows you to configure how the object is interpolated for owner and spectators.
Under the scenario your TickSmoother graphical had a set spectator interpolation of 2 and your PredictionManager an interpolation of 1, the spectated graphical object would take 2 ticks to get to it's goal after the PredictionManager ran the state 1 interpolation later than receiving it.
@@ -0,0 +1,9 @@
---
description: >-
In some cases you will want the player to be able to interact with
non-networked rigidbodies; these require a special component.
---
# Offline Rigidbodies
While prediction is in use, if you have a rigidbody object in your game that is not synchronized with the network it must contain an OfflineRigidbody component. Please review the [OfflineRigidbody component page](../../../fishnet-building-blocks/components/prediction/offlinerigidbody.md) for more information on it's uses.
@@ -0,0 +1,52 @@
---
description: >-
This class provides accurate simulations and re-simulations when applying
outside forces, most commonly through collisions.
---
# PredictionRigidbody
Using PredictionRigidbody is very straight forward. In short, you move from applying force changes to the rigidbody onto the PredictionRigidbody instance instead.
View the [Creating Code](creating-code/) guide for using PredictionRigidbody in your replicate and reconcile methods.
## Using outside the script
As mentioned above you should always apply forces to the PredictionRigidbody rather than the Rigidbody directly. Our first guides demonstrate how to do this within the replicate method, as well how to reconcile using the PredictionRigidbody, but do not show how to add forces from outside scripts, such as a bumper in your game.
There is virtually no complexity to adding outside forces other than remembering to add them again, to the PredictionRigidbody and not the Rigidbody.
The example below is what it might look like if using a trigger on a world object to repel the player.
{% hint style="warning" %}
For triggers and collisions to work properly with prediction you must use our NetworkTrigger/NetworkCollision components. Otherwise, due to a Unity limitation, such interactions would not work. You can learn more about those components here.
{% endhint %}
{% hint style="success" %}
Fun fact: Fish-Networking is the only framework that has the ability to simulate Enter/Exit events with prediction; not even Fusion does this!
{% endhint %}
```csharp
private void NetworkTrigger_OnEnter(Collider other)
{
// Add upward impulse when hitting this trigger.
if (other.TryGetComponent<RigidbodyPlayer>(out rbPlayer))
rbPlayer.PredictionRigidbody.AddForce(Vector3.up, ForceMode.Impulse);
// Do not call Simulate on the PredictionRigidbody here. That should only be done
// within the replicate method.
}
```
There is a fair chance a number of controllers will want to set velocities directly as well. PredictionRigidbody does support this. We encourage you to review the [PredictionRigidbody API](https://fish-networking.com/FishNet/api/api/FishNet.Object.Prediction.PredictionRigidbody.html#methods) for all available functionality.
Our last example demonstrates setting velocities directly.
{% hint style="info" %}
You would still call PredictionRigidbody.Simulate() regardless of how velocities are set.
{% endhint %}
```csharp
float horizontal = data.Horizontal;
Vector3 velocity = new Vector3(data.Horizontal, 0f, 0f) * _moveRate;
PredictionRigidbody.Velocity(velocity);
```
@@ -0,0 +1,97 @@
---
description: >-
Using each NetworkCollider component is the same, and can be used very similar
to Unity callbacks.
---
# Using NetworkColliders
When using client-side prediction the NetworkCollider components are designed to accurately dispatch Enter, Stay, and Exit callbacks for collisions and triggers.
All events have the same name across each component: NetworkCollider, NetworkCollider2D, NetworkTrigger, and NetworkTrigger2D. What is returned in each event naturally will vary, depending on which component you use. For example, the NetworkCollision component will return _Collider_ for what was intersected, while NetworkCollision2D will return _Collider2D._
{% hint style="warning" %}
Our OnStay callback does not return _Collision_ as you would expect with the Unity version. Due to Unity limitations we are unable to provide a Collision callback without hotpath allocations.
If you require Collision to be returned please use the Unity OnStay callback instead; this will function properly with prediction.
{% endhint %}
## Callbacks
**OnEnter** is invoked when this collider has entered the bounds of another collider.
**OnStay** is invoked when this collider remains in contact with another.
**OnExit** is invoked when this collider has exited the bounds of another collider.
{% hint style="info" %}
Keep in mind each callback description varies if you are using Collsion, Trigger, or their 2D counterparts. None-the-less, they behave just like Unity callbacks.
{% endhint %}
## Usage
This example script is placed on the player object. Our script demonstrates subscribing to all three events and using the `OnEnter` event to play a sound, as well applying forces to 'this' player.
```csharp
// We are assuming this script inherits NetworkBehaviour.
[SerializeField]
private AudioSource _hitSound;
private NetworkCollision _networkCollision;
private void Awake()
{
// Get the NetworkCollision component placed on this object.
// You can place the component anywhere you would normally
// use Unity collider callbacks!
_networkCollision = GetComponent<NetworkCollision>();
// Subscribe to the desired collision event
_networkCollision.OnEnter += NetworkCollisionEnter;
_networkCollision.OnStay += NetworkCollisionStay;
_networkCollision.OnExit += NetworkCollisionExit;
}
private void OnDestroy()
{
// Since the NetworkCollider is placed on the same object as
// this script we do not need to unsubscribe; the callbacks
// will be destroyed with the object.
// But if your NetworkCollider resides on another object you
// likely will want to unsubscribe to your events as well as shown.
if (_networkCollision != null)
{
_networkCollision.OnEnter -= NetworkCollisionEnter;
_networkCollision.OnStay -= NetworkCollisionStay;
_networkCollision.OnExit -= NetworkCollisionExit;
}
}
private void NetworkCollisionEnter(Collider other)
{
// Only play the sound effect when not currently reconciling.
// If you were to play the sound also during reconciliations
// the audio would execute each reconcile, each tick, until the
// player was no longer reconciling into the collider.
if (!base.PredictionManager.IsReconciling)
_hitSound.Play();
// Always apply velocity to this player on enter, even if reconciling.
PlayerMover pm = GetComponent<PlayerMover>();
// For this example we are pushing away from the other object.
Vector3 dir = (transform.position - other.gameObject.transform).normalized;
pm.PredictionRigidbody.AddForce(dir * 50f, ForceMode.Impulse);
}
private void NetworkCollisionStay(Collider other)
{
// Handle collision stay logic
}
private void NetworkCollisionExit(Collider other)
{
// Handle collision exit logic (e.g., deactivate visual effects)
}
```
You may be wondering what is PredictionRigidbody and why did we apply forces to that instead of the Rigidbody directly. PredictionRigidbody(and 2D) is a mandatory but still simple way to apply forces to predicted rigidbodies. You can learn more about it's usage [here](predictionrigidbody.md).
@@ -0,0 +1,15 @@
---
description: >-
Client-Side Prediction allows clients to perform actions in real-time while
maintaining server authority.
---
# What Is Client-Side Prediction
Client-side prediction is a technique used to move in real-time on clients, providing responsiveness actions, while also ensuring such actions cannot be cheated. From here out, we will refer to client-side prediction as CSP.
During your development you may also hear the term 'server authoritative movement'. CSP is a form of server authoritative movement, but they are not the same.
As mentioned CSP allows the client to move in real-time while also ensuring they cannot cheat. Some server authoritative movements will ensure the client cannot cheat but does so by moving on the server only and relaying the results to clients. While both work, the latter of moving on the server and then relaying will result in the client to have a delay based on their latency.
Having such a delay would be unfair to those with higher latency and could ruin the experience for players if your game is expected to have responsive movement. This is why having CSP built into Fish-Networking is so great!