using FishNet; using FishNet.Object; using FishNet.Object.Prediction; using FishNet.Transporting; using UnityEngine; /* * * See TransformPrediction.cs for more detailed notes. * */ namespace FishNet.Example.Prediction.Rigidbodies { public class RigidbodyPrediction : NetworkBehaviour { #region Types. public struct MoveData : IReplicateData { public bool Jump; public float Horizontal; public float Vertical; public MoveData(bool jump, float horizontal, float vertical) { Jump = jump; Horizontal = horizontal; Vertical = vertical; _tick = 0; } private uint _tick; public void Dispose() { } public uint GetTick() => _tick; public void SetTick(uint value) => _tick = value; } public struct ReconcileData : IReconcileData { public Vector3 Position; public Quaternion Rotation; public Vector3 Velocity; public Vector3 AngularVelocity; public ReconcileData(Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity) { Position = position; Rotation = rotation; Velocity = velocity; AngularVelocity = angularVelocity; _tick = 0; } private uint _tick; public void Dispose() { } public uint GetTick() => _tick; public void SetTick(uint value) => _tick = value; } #endregion #region Serialized. [SerializeField] private float _jumpForce = 15f; [SerializeField] private float _moveRate = 15f; #endregion #region Private. /// /// Rigidbody on this object. /// private Rigidbody _rigidbody; /// /// Next time a jump is allowed. /// private float _nextJumpTime; /// /// True to jump next frame. /// private bool _jump; #endregion #region Predicted spawning. /// /// Prefab to spawn for predicted spawning. /// public NetworkObject BulletPrefab; /// /// True if a spawn is queued from input. /// private bool _spawnBullet; /// /// True if a despawn is queued from input. /// private bool _despawnBullet; /// /// Last spawned bullet. Used to test predicted despawn. /// private NetworkObject _lastSpawnedBullet; #endregion private void Awake() { _rigidbody = GetComponent(); InstanceFinder.TimeManager.OnTick += TimeManager_OnTick; InstanceFinder.TimeManager.OnPostTick += TimeManager_OnPostTick; } private void OnDestroy() { if (InstanceFinder.TimeManager != null) { InstanceFinder.TimeManager.OnTick -= TimeManager_OnTick; InstanceFinder.TimeManager.OnPostTick -= TimeManager_OnPostTick; } } public override void OnStartClient() { base.OnStartClient(); base.PredictionManager.OnPreReplicateReplay += PredictionManager_OnPreReplicateReplay; } public override void OnStopClient() { base.OnStopClient(); base.PredictionManager.OnPreReplicateReplay -= PredictionManager_OnPreReplicateReplay; } private void Update() { if (base.IsOwner) { if (Input.GetKeyDown(KeyCode.RightAlt)) { _rigidbody.velocity = Vector3.zero; _rigidbody.angularVelocity = Vector3.zero; } if (Input.GetKeyDown(KeyCode.Space) && Time.time > _nextJumpTime) { _nextJumpTime = Time.time + 1f; _jump = true; } else if (Input.GetKeyDown(KeyCode.LeftShift)) { _spawnBullet = true; } //else if (Input.GetKeyDown(KeyCode.LeftAlt)) //{ // _despawnBullet = true; //} } } /// /// Called every time any predicted object is replaying. Replays only occur for owner. /// Currently owners may only predict one object at a time. /// private void PredictionManager_OnPreReplicateReplay(uint arg1, PhysicsScene arg2, PhysicsScene2D arg3) { /* Server does not replay so it does * not need to add gravity. */ if (!base.IsServer) AddGravity(); } private void TimeManager_OnTick() { if (base.IsOwner) { Reconciliation(default, false); BuildMoveData(out MoveData md); Move(md, false); //Predicted spawning example. TryDespawnBullet(); TrySpawnBullet(); } if (base.IsServer) { Move(default, true); } /* Server and all clients must add the additional gravity. * Adding gravity is not necessarily required in general but * to make jumps more snappy extra gravity is added per tick. * All clients and server need to simulate the gravity to keep * prediction equal across the network. */ AddGravity(); } private void TimeManager_OnPostTick() { /* Reconcile is sent during PostTick because we * want to send the rb data AFTER the simulation. */ if (base.IsServer) { ReconcileData rd = new ReconcileData(transform.position, transform.rotation, _rigidbody.velocity, _rigidbody.angularVelocity); Reconciliation(rd, true); } } /// /// Builds a MoveData to use within replicate. /// /// private void BuildMoveData(out MoveData md) { md = default; float horizontal = Input.GetAxisRaw("Horizontal"); float vertical = Input.GetAxisRaw("Vertical"); if (horizontal == 0f && vertical == 0f && !_jump) return; md = new MoveData(_jump, horizontal, vertical); _jump = false; } /// /// PredictedObject example (unpolished) /// private void TrySpawnBullet() { if (_spawnBullet) { _spawnBullet = false; NetworkObject nob = Instantiate(BulletPrefab, transform.position + (transform.forward * 1f), transform.rotation); //Set last spawned to test destroy with ALT key. _lastSpawnedBullet = nob; //Set force to 100f at current forward. PredictedBullet bt = nob.GetComponent(); bt.SetStartingForce(transform.forward * 20f); //Spawn client side, which will send the predicted spawn to server. base.Spawn(nob, base.Owner); } } /// /// PredictedObject example (unpolished) /// private void TryDespawnBullet() { if (_despawnBullet) { _despawnBullet = false; _lastSpawnedBullet?.Despawn(); } } /// /// Adds gravity to the rigidbody. /// private void AddGravity() { _rigidbody.AddForce(Physics.gravity * 2f); } [Replicate] private void Move(MoveData md, bool asServer, Channel channel = Channel.Unreliable, bool replaying = false) { Vector3 forces = new Vector3(md.Horizontal, 0f, md.Vertical) * _moveRate; _rigidbody.AddForce(forces); if (md.Jump) _rigidbody.AddForce(new Vector3(0f, _jumpForce, 0f), ForceMode.Impulse); } [Reconcile] private void Reconciliation(ReconcileData rd, bool asServer, Channel channel = Channel.Unreliable) { transform.position = rd.Position; transform.rotation = rd.Rotation; _rigidbody.velocity = rd.Velocity; _rigidbody.angularVelocity = rd.AngularVelocity; } } }