using FishNet.Managing.Timing;
using FishNet.Object;
using FishNet.Object.Prediction;
using FishNet.Transporting;
using FishNet.Utility.Template;
using UnityEngine;
/* Note: the graphical object for this predicted NetworkObject is unset.
* This is because currently the NetworkObject only allows setting of one
* graphical object, but there are three things that move independently.
*
* In version 4.5.8 there will be an option to support multiple graphical objects
* and this demo will be updated. There are plans to release 4.5.8 with only this improvement
* to get the update out fast as possible. */
namespace FishNet.Demo.Prediction.Rigidbodies
{
public class RigidbodyPrediction : TickNetworkBehaviour
{
#region Types.
public struct ReplicateData : IReplicateData
{
public ReplicateData(Vector2 input, bool fire)
{
Input = input;
Fire = fire;
_tick = 0;
}
///
/// Current movement directions held.
///
public Vector2 Input;
///
/// True to fire.
///
public bool Fire;
///
/// Tick is set at runtime. There is no need to manually assign this value.
///
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
public struct ReconcileData : IReconcileData
{
public ReconcileData(PredictionRigidbody root, PredictionRigidbody frontWheel, PredictionRigidbody rearWheel, uint boostStartTick)
{
Root = root;
FrontWheel = frontWheel;
RearWheel = rearWheel;
BoostStartTick = boostStartTick;
_tick = 0;
}
///
/// PredictionRigidbody on the root.
///
public PredictionRigidbody Root;
///
/// PredictionRigidbody controlling the front wheel.
///
public PredictionRigidbody FrontWheel;
///
/// PredictionRigidbody controlling the rear wheel.
///
public PredictionRigidbody RearWheel;
///
/// Tick which the boost started.
///
public uint BoostStartTick;
///
/// Tick is set at runtime. There is no need to manually assign this value.
///
private uint _tick;
/* You do not need to dispose PredictionRigidbody when used with prediction.
* These references will automatically use pooling to prevent garbage allocations! */
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
#endregion
[SerializeField]
private Rigidbody _frontWheelRigidbody;
[SerializeField]
private Rigidbody _rearWheelRigidbody;
[SerializeField]
private float _boostDuration = 1f;
[SerializeField]
private float _boostForce = 20f;
[SerializeField]
private float _moveRate = 4f;
[SerializeField]
private float _turnRate = 4f;
///
/// Root of the vehicle.
///
private PredictionRigidbody _root = new();
///
/// Drives turning (front wheels).
///
private PredictionRigidbody _frontWheel = new();
///
/// Drives acceleration (rear wheels).
///
private PredictionRigidbody _rearWheel = new();
///
/// Tick which the boost started.
///
private uint _boostStartTick = TimeManager.UNSET_TICK;
///
/// Tick on the last replicate.
///
private uint _lastReplicateTick;
///
/// Next tick the controller is allowed to predicted fire.
///
private uint _nextAllowedFireTick;
private void Awake()
{
_root.Initialize(GetComponent());
_frontWheel.Initialize(_frontWheelRigidbody);
_rearWheel.Initialize(_rearWheelRigidbody);
}
public override void OnStartNetwork()
{
//Rigidbodies need tick and postTick.
base.SetTickCallbacks(TickCallback.Tick | TickCallback.PostTick);
}
protected override void TimeManager_OnTick()
{
PerformReplicate(BuildMoveData());
CreateReconcile();
}
///
/// Returns replicate data to send as the controller.
///
private ReplicateData BuildMoveData()
{
/* Only the controller needs to build move data.
* This could be the server if the server if no owner, for example
* such as AI, or the owner of the object. */
if (!base.IsOwner) return default;
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
//To keep things simple firing is done by holding left shift.
bool fire = Input.GetKey(KeyCode.LeftShift);
ReplicateData md = new(new(horizontal, vertical), fire);
return md;
}
///
/// Creates a reconcile that is sent to clients.
///
public override void CreateReconcile()
{
/* Both the server and client should create reconcile data.
* The client will use their copy as a fallback if they do not
* get data from the server, such as a dropped packet.
*
* The client will not reconcile unless it receives at least one
* reconcile packet from the server for the tick. */
/* You do not have to reconcile every tick if you wish to
* save bandwidth/perf, or simply feel as though it's not needed
* for your game type.
*
* Even when not reconciling every tick it's still recommended
* to build the reconcile as client; this cost is very little.*/
/* This is an example of only sending a reconcile occasionally
* if the server. Simply uncomment the if statement below to
* test this behavior. */
// if (base.IsServerStarted)
// {
// //Exit early if 10 ticks have not passed.
// if (base.TimeManager.LocalTick % 10 != 0) return;
// }
//Build the data using current information and call the reconcile method.
ReconcileData rd = new(_root, _frontWheel, _rearWheel, _boostStartTick);
PerformReconcile(rd);
}
[Replicate]
private void PerformReplicate(ReplicateData rd, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
uint rdTick = rd.GetTick();
_lastReplicateTick = rdTick;
/* Since rigidbodies typically carry inertia you do not necessarily need
* to predict in the future; rigidbodies will continue to move along
* the same path anyway.
*
* You can still predict inputs a couple ticks like we did in the
* CharacterController example, if you find doing so creates
* better results. */
Vector3 turningForce = new(rd.Input.x * _turnRate, 0f, 0f);
Vector3 forwardForce = new(0f, 0f, rd.Input.y * _moveRate);
/* If boostStartTick is not unset then a boost is started.
*
* Make sure that the current data tick is at least equal
* to boost tick before adding boost.
* This is done in the scenario a boost happened outside replay,
* we don't want to boost during a replay before the boost started. */
if (_boostStartTick != TimeManager.UNSET_TICK && rdTick >= _boostStartTick)
{
//Add boost to forward force.
forwardForce += new Vector3(0f, 0f, _boostForce);
uint boostTimeToTicks = base.TimeManager.TimeToTicks(_boostDuration, TickRounding.RoundUp);
//This is when boost will end.
uint endTick = (_boostStartTick + boostTimeToTicks);
//Unset boost if tick is met.
if (rdTick >= endTick)
_boostStartTick = TimeManager.UNSET_TICK;
}
//Convert forwards based on root forward.
Transform rootTransform = _root.Rigidbody.transform;
turningForce = rootTransform.TransformDirection(turningForce);
forwardForce = rootTransform.TransformDirection(forwardForce);
//Flip turning if vehicle is also flipped.
if (rootTransform.up.y <= -0.1f)
turningForce *= -1f;
/* Add turning and forward force.
*
* Notice that forces are NOT multiplied by
* delta. Just like Unity physics, predictionRigidbodies
* do not include delta in calculated forces. */
_frontWheel.AddForce(turningForce);
_rearWheel.AddForce(forwardForce);
_root.Simulate();
_frontWheel.Simulate();
_rearWheel.Simulate();
}
[Reconcile]
private void PerformReconcile(ReconcileData rd, Channel channel = Channel.Unreliable)
{
/* Reconcile boosted start tick. Even though the NetworkTrigger will
* invoke again if a replayed replicate pushes the vehicle through
* the trigger, it will not if the vehicle is in the trigger before the reconcile,
* as well after; since they never left, enter will not be called.*/
_boostStartTick = rd.BoostStartTick;
//Reconcile all the rigidbodies.
_root.Reconcile(rd.Root);
_frontWheel.Reconcile(rd.FrontWheel);
_rearWheel.Reconcile(rd.RearWheel);
}
///
/// Sets boosted state for a number of ticks.
///
public void SetBoosted()
{
/* Boost start is set to whatever tick was last replicated.
* Replicate is called every tick, so if the controller hits
* the collider during a replay the tick will be whatever replicate
* is being replayed, if outside a replay it will be the current
* tick.
*
* If owner or server the current tick would be localTick, otherwise
* it will be server tick. */
_boostStartTick = _lastReplicateTick;
}
}
}