新增FishNet文档
This commit is contained in:
@@ -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
|
||||
|
||||
+198
@@ -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.
|
||||
+245
@@ -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 %}
|
||||
+110
@@ -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);
|
||||
}
|
||||
}
|
||||
```
|
||||
+58
@@ -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 %}
|
||||
+110
@@ -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 %}
|
||||
+190
@@ -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();
|
||||
}
|
||||
|
||||
```
|
||||
Reference in New Issue
Block a user