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; } } }