using System.Collections.Generic; using FishNet.Object; using FishNet.Object.Prediction; using FishNet.Transporting; using FishNet.Utility.Template; using UnityEngine; namespace FishNet.Demo.Prediction.CharacterControllers { public class MovingPlatform : TickNetworkBehaviour { #region Types. public struct ReplicateData : IReplicateData { public ReplicateData(uint unused = 0) { _tick = 0; } /// /// 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(Vector3 position, byte goalIndex) { Position = position; GoalIndex = goalIndex; _tick = 0; } /// /// Position of the character. /// public Vector3 Position; /// /// Current vertical velocity. /// /// Used to simulate jumps and falls. public byte GoalIndex; /// /// 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; } #endregion [SerializeField] private float _moveRate = 4f; /// /// Goal to move towards. /// private byte _goalIndex; /// /// Goals to move towards. /// private List _goals = new(); private void Awake() { const float offset = 5f; Vector3 position = transform.position; _goals.Add(position + new Vector3(0f, 0f, offset)); _goals.Add(position + new Vector3(0f, 0f, -offset)); } public override void OnStartNetwork() { base.SetTickCallbacks(TickCallback.Tick); } protected override void TimeManager_OnTick() { PerformReplicate(default); CreateReconcile(); } /// /// Creates a reconcile that is sent to clients. /// public override void CreateReconcile() { ReconcileData rd = new(transform.position, _goalIndex); PerformReconcile(rd); } [Replicate] private void PerformReplicate(ReplicateData rd, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable) { /* Move logic is always called regardless of state. * * This means that move will be called when replaying after a reconcile, * as well during OnTick, as well even if there is no data being received * from the server (IsFuture). * * By doing this we allow the platform to be ahead of what the client has * received by the server. * * When observed the client will actually see the platform * ahead of the server depending on their ping, being more ahead with higher pings. * This is the desired outcome as that's where the platform will be by the time * the client sends input to the server. If the platform was not ahead of the server * then when the client perhaps jumped onto it, the platform would actually be further * on the server then where the client observed it. In result, the client would snap to * a correct position when landing on the platform. * * If you want to visually see this simple uncomment the line below. Be sure to add * latency to make the correction more noticeable. */ // if (state.IsFuture()) // return; //Always use the tickDelta as your delta when performing actions inside replicate. float delta = (float)base.TimeManager.TickDelta; Vector3 goal = _goals[_goalIndex]; Vector3 next = Vector3.MoveTowards(transform.position, goal, delta * _moveRate); transform.position = next; if (next == goal) { _goalIndex++; if (_goalIndex >= _goals.Count) _goalIndex = 0; } } [Reconcile] private void PerformReconcile(ReconcileData rd, Channel channel = Channel.Unreliable) { transform.position = rd.Position; _goalIndex = rd.GoalIndex; } } }