[Add] FishNet
This commit is contained in:
+499
@@ -0,0 +1,499 @@
|
||||
using System;
|
||||
using FishNet.Component.Prediction;
|
||||
using FishNet.Connection;
|
||||
using FishNet.Object.Prediction;
|
||||
using FishNet.Transporting;
|
||||
using FishNet.Utility.Template;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Demo.Prediction.CharacterControllers
|
||||
{
|
||||
public class CharacterControllerPrediction : TickNetworkBehaviour
|
||||
{
|
||||
#region Types.
|
||||
/// <summary>
|
||||
/// One-time inputs accumulated over frames between ticks.
|
||||
/// </summary>
|
||||
public struct OneTimeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// True to jump.
|
||||
/// </summary>
|
||||
public bool Jump;
|
||||
|
||||
/// <summary>
|
||||
/// Unset inputs.
|
||||
/// </summary>
|
||||
public void ResetState()
|
||||
{
|
||||
Jump = false;
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReplicateData : IReplicateData
|
||||
{
|
||||
public ReplicateData(Vector2 input, bool run, OneTimeInput oneTimeInputs)
|
||||
{
|
||||
OneTimeInputs = oneTimeInputs;
|
||||
Input = input;
|
||||
Run = run;
|
||||
|
||||
_tick = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if Jump input was pressed for the tick.
|
||||
/// </summary>
|
||||
public OneTimeInput OneTimeInputs;
|
||||
/// <summary>
|
||||
/// Current movement directions held.
|
||||
/// </summary>
|
||||
public Vector2 Input;
|
||||
/// <summary>
|
||||
/// True if run is held.
|
||||
/// </summary>
|
||||
public readonly bool Run;
|
||||
/// <summary>
|
||||
/// Tick is set at runtime. There is no need to manually assign this value.
|
||||
/// </summary>
|
||||
private uint _tick;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
OneTimeInputs.ResetState();
|
||||
}
|
||||
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
|
||||
public struct ReconcileData : IReconcileData
|
||||
{
|
||||
public ReconcileData(Vector3 position, float verticalVelocity, float stamina, MovingPlatform currentPlatform)
|
||||
{
|
||||
Position = position;
|
||||
VerticalVelocity = verticalVelocity;
|
||||
Stamina = stamina;
|
||||
CurrentPlatform = currentPlatform;
|
||||
|
||||
_tick = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position of the character.
|
||||
/// </summary>
|
||||
public Vector3 Position;
|
||||
/// <summary>
|
||||
/// Current vertical velocity.
|
||||
/// </summary>
|
||||
/// <remarks>Used to simulate jumps and falls.</remarks>
|
||||
public float VerticalVelocity;
|
||||
/// <summary>
|
||||
/// Amount of stamina remaining to run or jump.
|
||||
/// </summary>
|
||||
public float Stamina;
|
||||
public MovingPlatform CurrentPlatform;
|
||||
/// <summary>
|
||||
/// Tick is set at runtime. There is no need to manually assign this value.
|
||||
/// </summary>
|
||||
private uint _tick;
|
||||
public void Dispose() { }
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Invokes whenever NetworkStart is called for owner.
|
||||
/// </summary>
|
||||
public static event Action<CharacterControllerPrediction> OnOwner;
|
||||
/// <summary>
|
||||
/// Current stamina remaining.
|
||||
/// </summary>
|
||||
public float Stamina { get; private set; }
|
||||
/// <summary>
|
||||
/// Amount of force for jumping.
|
||||
/// </summary>
|
||||
[Tooltip("Amount of force for jumping.")]
|
||||
[SerializeField]
|
||||
private float _jumpForce = 30f;
|
||||
/// <summary>
|
||||
/// How quickly to move.
|
||||
/// </summary>
|
||||
[Tooltip("How quickly to move.")]
|
||||
[SerializeField]
|
||||
private float _moveRate = 4f;
|
||||
/// <summary>
|
||||
/// Current vertical velocity.
|
||||
/// </summary>
|
||||
private float _verticalVelocity;
|
||||
/// <summary>
|
||||
/// One-time inputs accumulated over frames between ticks.
|
||||
/// </summary>
|
||||
private OneTimeInput _oneTimeInputs = new();
|
||||
/// <summary>
|
||||
/// Last data which was supplied during replicate outside of reconcile.
|
||||
/// </summary>
|
||||
private ReplicateData _lastTickedReplicateData = default;
|
||||
/// <summary>
|
||||
/// Current platform the player is on.
|
||||
/// </summary>
|
||||
private MovingPlatform _currentPlatform;
|
||||
/// <summary>
|
||||
/// Reference to the CharacterController component.
|
||||
/// </summary>
|
||||
private CharacterController _characterController;
|
||||
/// <summary>
|
||||
/// NetworkTrigger on this character; used to detact platforms.
|
||||
/// </summary>
|
||||
private NetworkTrigger _characterTrigger;
|
||||
/// <summary>
|
||||
/// maximum amount of stamina allowed.
|
||||
/// </summary>
|
||||
public const float Maximum_Stamina = 50f;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_characterController = GetComponent<CharacterController>();
|
||||
|
||||
_characterTrigger = GetComponentInChildren<NetworkTrigger>();
|
||||
_characterTrigger.OnEnter += CharacterTrigger_OnEnter;
|
||||
_characterTrigger.OnExit += CharacterTrigger_OnExit;
|
||||
|
||||
SetTickCallbacks(TickCallback.Tick | TickCallback.PostTick);
|
||||
}
|
||||
|
||||
public override void OnOwnershipClient(NetworkConnection prevOwner)
|
||||
{
|
||||
if (IsOwner)
|
||||
OnOwner?.Invoke(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
SetOneTimeInputs();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks setting inputs which are one-time(not held).
|
||||
/// </summary>
|
||||
private void SetOneTimeInputs()
|
||||
{
|
||||
if (!IsOwner)
|
||||
return;
|
||||
|
||||
/* Check to jump. */
|
||||
if (Input.GetKeyDown(KeyCode.Space))
|
||||
_oneTimeInputs.Jump = true;
|
||||
}
|
||||
|
||||
protected override void TimeManager_OnTick()
|
||||
{
|
||||
PerformReplicate(BuildMoveData());
|
||||
}
|
||||
|
||||
protected override void TimeManager_OnPostTick()
|
||||
{
|
||||
/* Typically a CharacterController could send a reconcile within
|
||||
* OnTick, after completing all moving logic. However, this demo
|
||||
* uses a NetworkTrigger to jump on platforms, which we need the results
|
||||
* from before sending a reconcile. The NetworkTrigger/Collider components
|
||||
* trace after simulation which occurs between OnTick, and OnPostTick, therefor
|
||||
* the trace would have happened by the time OnPostTick is called. */
|
||||
CreateReconcile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns replicate data to send as the controller.
|
||||
/// </summary>
|
||||
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 (!IsOwner)
|
||||
return default;
|
||||
|
||||
float horizontal = Input.GetAxisRaw("Horizontal");
|
||||
float vertical = Input.GetAxisRaw("Vertical");
|
||||
// Run when left shift is held.
|
||||
bool run = Input.GetKey(KeyCode.LeftShift);
|
||||
|
||||
ReplicateData md = new(new(horizontal, vertical), run, _oneTimeInputs);
|
||||
|
||||
// Reset one tine inputs since they've been processed for the tick.
|
||||
_oneTimeInputs.ResetState();
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a reconcile that is sent to clients.
|
||||
/// </summary>
|
||||
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(transform.localPosition, _verticalVelocity, Stamina, _currentPlatform);
|
||||
PerformReconcile(rd);
|
||||
}
|
||||
|
||||
[Replicate]
|
||||
private void PerformReplicate(ReplicateData rd, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// Always use the tickDelta as your delta when performing actions inside replicate.
|
||||
float delta = (float)TimeManager.TickDelta;
|
||||
bool useDefaultForces = false;
|
||||
|
||||
/* When client only run some checks to
|
||||
* further predict the clients future movement.
|
||||
* This can keep the object more inlined with real-time by
|
||||
* guessing what the clients input might be before we
|
||||
* actually receive it.
|
||||
*
|
||||
* Doing this does risk a chance of graphical jitter in the
|
||||
* scenario a de-synchronization occurs, but if only predicting
|
||||
* a couple ticks the chances are low. */
|
||||
// See https://fish-networking.gitbook.io/docs/manual/guides/prediction/version-2/creating-code/predicting-states
|
||||
if (!IsServerStarted && !IsOwner)
|
||||
{
|
||||
/* If ticked then set last ticked value.
|
||||
* Ticked means the replicate is being run from the tick cycle, more
|
||||
* specifically NOT from a replay/reconcile. */
|
||||
if (state.ContainsTicked())
|
||||
{
|
||||
/* Dispose of old should it have anything that needs to be cleaned up.
|
||||
* If you are only using value types in your data you do not need to call Dispose.
|
||||
* You must implement dispose manually to cache any non-value types, if you wish. */
|
||||
_lastTickedReplicateData.Dispose();
|
||||
// Set new.
|
||||
_lastTickedReplicateData = rd;
|
||||
}
|
||||
/* In the future means there is no way the data can be known to this client
|
||||
* yet. For example, the client is running this script locally and due to
|
||||
* how networking works, they have not yet received the latest information from
|
||||
* the server.
|
||||
*
|
||||
* If in the future then we are only going to predict up to
|
||||
* a certain amount of ticks in the future. This is us assuming that the
|
||||
* server (or client which owns this in this case) is going to use the
|
||||
* same input for at least X number of ticks. You can predict none, or as many
|
||||
* as you like, but the more inputs you predict the higher likeliness of guessing
|
||||
* wrong. If you do however predict wrong often smoothing will cover up the mistake. */
|
||||
else if (state.IsFuture())
|
||||
{
|
||||
/* Predict up to 1 tick more. */
|
||||
if (rd.GetTick() - _lastTickedReplicateData.GetTick() > 1)
|
||||
{
|
||||
useDefaultForces = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If here we are predicting the future. */
|
||||
|
||||
/* You likely do not need to dispose rd here since it would be default
|
||||
* when state is 'not created'. We are simply doing it for good practice, should your ReplicateData
|
||||
* contain any garbage collection. */
|
||||
rd.Dispose();
|
||||
|
||||
rd = _lastTickedReplicateData;
|
||||
|
||||
/* There are some fields you might not want to predict, for example
|
||||
* jump. The odds of a client pressing jump two ticks in a row is unlikely.
|
||||
* The stamina check below would likely prevent such a scenario.
|
||||
*
|
||||
* We're going to unset jump for this reason. */
|
||||
rd.OneTimeInputs.Jump = false;
|
||||
|
||||
/* Be aware that future predicting is not a one-size fits all
|
||||
* feature. How much you predict into the future, if at all, depends
|
||||
* on your game mechanics and your desired outcome. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 forces;
|
||||
|
||||
if (useDefaultForces)
|
||||
{
|
||||
/* Character controllers are a bit problematic with colliders.
|
||||
* If you were to pass Vector3.zero into the move then there's
|
||||
* a chance other colliders will clip through the characterController.
|
||||
* When this is combined with reconciles, it's practically gauranteed
|
||||
* this will happen.
|
||||
*
|
||||
* Because of this issue, if we are 'using default forces' we will apply a
|
||||
* very insignificant amount of force which makes the characterController
|
||||
* update properly. */
|
||||
forces = new(0f, -1f, 0f);
|
||||
}
|
||||
// Calculate forces.
|
||||
else
|
||||
{
|
||||
// Stamina regained over every second.
|
||||
const float regainedStamina = 25f;
|
||||
// Add stamina with every tick.
|
||||
ModifyStamina(regainedStamina * delta);
|
||||
|
||||
// Add gravity. Extra gravity is added for snappier jumps.
|
||||
_verticalVelocity += Physics.gravity.y * delta * 3f;
|
||||
//Cap gravity to -20f so the player doesn't fall too fast.
|
||||
if (_verticalVelocity < -40f)
|
||||
_verticalVelocity = -40f;
|
||||
|
||||
//Normalize direction so the player does not move faster at angles.
|
||||
rd.Input = rd.Input.normalized;
|
||||
|
||||
/* Typically speaking any modification which can affect your CSP (prediction) should occur
|
||||
* inside replicate. This is why we add/remove stamina, and move within replicate. */
|
||||
|
||||
//Default run multiplier.
|
||||
float runMultiplier;
|
||||
//Stamina required to run over a second.
|
||||
const float runStamina = 50f;
|
||||
if (rd.Run && TryRemoveStamina(runStamina * delta))
|
||||
runMultiplier = 1.5f;
|
||||
else
|
||||
runMultiplier = 1f;
|
||||
|
||||
//Stamina required to jump.
|
||||
const byte jumpStamina = 30;
|
||||
/* For consistent jumps set to jump force when jumping, rather
|
||||
* than add force onto current gravity. */
|
||||
if (rd.OneTimeInputs.Jump && TryRemoveStamina(jumpStamina))
|
||||
_verticalVelocity = _jumpForce;
|
||||
|
||||
forces = new Vector3(rd.Input.x, 0f, rd.Input.y) * (_moveRate * runMultiplier);
|
||||
//Add vertical velocity to forces.
|
||||
forces.y = _verticalVelocity;
|
||||
}
|
||||
|
||||
_characterController.Move(forces * delta);
|
||||
}
|
||||
|
||||
[Reconcile]
|
||||
private void PerformReconcile(ReconcileData rd, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
/* Simply set current values to as they are
|
||||
* in the reconcile data. */
|
||||
_verticalVelocity = rd.VerticalVelocity;
|
||||
Stamina = rd.Stamina;
|
||||
|
||||
/* It is VERY important to disable the CharacterController
|
||||
* component before updating its position. If you do not
|
||||
* use the disable/enable work-around below, the Transform
|
||||
* will show the correct position but the physics for
|
||||
* the CharacterController will remain at it's prior position
|
||||
* until the next simulate. */
|
||||
_characterController.enabled = false;
|
||||
|
||||
/* Even though the platform is traced for in replicate we must also
|
||||
* pass the current platform into the reconcile, and set our local value
|
||||
* to whatever is provided in the reconcile.
|
||||
*
|
||||
* This is done because we use local position to reset the character
|
||||
* and if the clients currentPlatform differs from what the server
|
||||
* had when reconciling the world position would be significantly
|
||||
* different due to the parent not aligning. */
|
||||
_currentPlatform = rd.CurrentPlatform;
|
||||
//Set transform parent after assigning current.
|
||||
SetParent();
|
||||
/* Update position AFTER setting the parent, otherwise
|
||||
* you would face a potentially huge positional de-sync
|
||||
* as mentioned above. */
|
||||
transform.localPosition = rd.Position;
|
||||
|
||||
_characterController.enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the trigger on this object enters another collider.
|
||||
/// </summary>
|
||||
private void CharacterTrigger_OnEnter(Collider c)
|
||||
{
|
||||
//We only care about moving platforms.
|
||||
if (!c.TryGetComponent(out MovingPlatform mp))
|
||||
return;
|
||||
|
||||
_currentPlatform = mp;
|
||||
SetParent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the trigger on this object exits another collider.
|
||||
/// </summary>
|
||||
private void CharacterTrigger_OnExit(Collider c)
|
||||
{
|
||||
if (c == null)
|
||||
return;
|
||||
if (!c.TryGetComponent(out MovingPlatform mp))
|
||||
return;
|
||||
|
||||
//Only check if already attached to a platform.
|
||||
if (_currentPlatform == null)
|
||||
return;
|
||||
//Not the current platform.
|
||||
if (_currentPlatform != mp)
|
||||
return;
|
||||
|
||||
//Is the current platform, unset.
|
||||
_currentPlatform = null;
|
||||
SetParent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates parent state based on current platforms value.
|
||||
/// </summary>
|
||||
private void SetParent()
|
||||
{
|
||||
if (_currentPlatform != null)
|
||||
transform.SetParent(_currentPlatform.transform);
|
||||
else
|
||||
transform.SetParent(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modifies stamina by adding or removing stamina.
|
||||
/// </summary>
|
||||
private void ModifyStamina(float value)
|
||||
{
|
||||
float next = Stamina + value;
|
||||
Stamina = Mathf.Clamp(next, 0f, Maximum_Stamina);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes stamina if enough stamina is available.
|
||||
/// </summary>
|
||||
/// <returns>>True if stamina was available and removed.</returns>
|
||||
private bool TryRemoveStamina(float value)
|
||||
{
|
||||
if (Stamina < value)
|
||||
return false;
|
||||
|
||||
Stamina -= value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user