Skip to content

Instantly share code, notes, and snippets.

@WolframGroetsch
Created November 21, 2023 19:27
Show Gist options
  • Select an option

  • Save WolframGroetsch/018a127820fe11867e1816dc3fb08608 to your computer and use it in GitHub Desktop.

Select an option

Save WolframGroetsch/018a127820fe11867e1816dc3fb08608 to your computer and use it in GitHub Desktop.
FishNet prediction movement code for KCC
public struct PlayerCharacterInputs // This struct only handles the local input and is used to pass it to SetInput method
{
public float MoveAxisForward;
public float MoveAxisRight;
public Vector3 CameraRotationEuler;
public bool JumpDown;
public bool JumpHeld;
public bool CrouchDown;
public bool CrouchUp;
public bool CrouchHeld;
public bool NoClipDown;
}
public struct MoveData : IReplicateData
{
public PlayerCharacterInputs Inputs;
public MoveData(PlayerCharacterInputs inputs)
{
Inputs = inputs;
_tick = 0;
}
//FishNet
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 Vector3 RotationEuler;
public Vector3 BaseVelocity;
public bool MustUnground;
public float MustUngroundTime;
public bool LastMovementIterationFoundAnyGround;
public CharacterTransientGroundingReport GroundingStatus;
public Vector3 AttachedRigidbodyVelocity;
public bool JumpConsumed;
public bool DoubleJumpConsumed;
public CharacterState CurrentCharacterState;
public ClimbingState CurrentClimbingState;
public float AnchoringTimer;
public ReconcileData(Vector3 position, Vector3 rotationEuler, Vector3 velocity,
bool mustUnground, float mustUngroundTime, bool lastMovementIterationFoundAnyGround,
CharacterTransientGroundingReport groundingStatus, Vector3 attachedRigidbodyVelocity,
bool jumpConsumed, bool doubleJumpConsumed,
CharacterState currentCharacterState, ClimbingState currentClimbingState,
float anchoringTimer)
{
Position = position;
RotationEuler = rotationEuler;
BaseVelocity = velocity;
MustUnground = mustUnground;
MustUngroundTime = mustUngroundTime;
LastMovementIterationFoundAnyGround = lastMovementIterationFoundAnyGround;
GroundingStatus = groundingStatus;
AttachedRigidbodyVelocity = attachedRigidbodyVelocity;
JumpConsumed = jumpConsumed;
DoubleJumpConsumed = doubleJumpConsumed;
CurrentCharacterState = currentCharacterState;
CurrentClimbingState = currentClimbingState;
AnchoringTimer = anchoringTimer;
_tick = 0;
}
//FishNet
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
public override void OnStartNetwork()
{
base.OnStartNetwork();
base.TimeManager.OnTick += TimeManager_OnTick;
base.TimeManager.OnPostTick += TimeManager_OnPostTick;
KinematicCharacterSystem.Settings.AutoSimulation = false;
KinematicCharacterSystem.Settings.Interpolate = false;
}
public override void OnStopNetwork()
{
base.OnStopNetwork();
if (base.TimeManager != null)
{
base.TimeManager.OnTick -= TimeManager_OnTick;
base.TimeManager.OnPostTick -= TimeManager_OnPostTick;
}
}
private void TimeManager_OnTick()
{
Move(BuildMoveData());
// Run server sided checks
if (base.IsServerStarted)
{
CheckForFellOffIsland();
CheckForFallDamage();
}
}
private void TimeManager_OnPostTick()
{
if (IsServerStarted && base.TimeManager.Tick % 1 == 0) // reconcile as server only every 5th tick
{
KinematicCharacterMotorState state = Motor.GetState();
ReconcileData rd = new ReconcileData(state.Position, state.Rotation.eulerAngles, state.BaseVelocity,
state.MustUnground, state.MustUngroundTime, state.LastMovementIterationFoundAnyGround,
state.GroundingStatus, state.AttachedRigidbodyVelocity,
_jumpConsumed, _doubleJumpConsumed,
CurrentCharacterState, _internalClimbingState,
_anchoringTimer);
Reconciliation(rd);
}
}
private MoveData BuildMoveData()
{
if (!base.IsOwner)
return default;
_playerInput.GetMoveInputs(out PlayerCharacterInputs inputs);
MoveData md = new MoveData(inputs);
return md;
}
[ReplicateV2]
private void Move(MoveData md, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
if (!base.IsOwner && !base.IsServerStarted)
{
//If new data then set lastmovedata.
if (state == ReplicateState.UserCreated || state == ReplicateState.ReplayedUserCreated)
{
_lastMoveData = md;
_lastMoveData.Inputs.MoveAxisRight = md.Inputs.MoveAxisRight * predictedMovementFalloff;
_lastMoveData.Inputs.MoveAxisForward = md.Inputs.MoveAxisForward * predictedMovementFalloff;
}
//If not new data then replay last inputs until new data arrives.
else
{
//Last data must be set. We check here camera rotation, because if it is zero it is definitely unset.
if (_lastMoveData.Inputs.CameraRotationEuler != Vector3.zero)
{
/* The tick will increase even if the data is unset.
* Cache the tick, set md to last data, then reapply the tick. */
uint tick = md.GetTick();
md = _lastMoveData;
md.SetTick(tick);
}
}
}
SetInputs(md.Inputs);
float deltaTime = (float)TimeManager.TickDelta;
AdriftKCCUpdater.SimulateSingleCharacter(this.Motor, deltaTime);
}
[ReconcileV2]
private void Reconciliation(ReconcileData rd, Channel channel = Channel.Unreliable)
{
CurrentCharacterState = rd.CurrentCharacterState;
_internalClimbingState = rd.CurrentClimbingState;
KinematicCharacterMotorState state = Motor.GetState();
state.Position = rd.Position;
state.Rotation = Quaternion.Euler(rd.RotationEuler.x, rd.RotationEuler.y, rd.RotationEuler.z);
state.BaseVelocity = rd.BaseVelocity;
state.MustUnground = rd.MustUnground;
state.MustUngroundTime = rd.MustUngroundTime;
state.LastMovementIterationFoundAnyGround = rd.LastMovementIterationFoundAnyGround;
state.GroundingStatus = rd.GroundingStatus;
state.AttachedRigidbodyVelocity = rd.AttachedRigidbodyVelocity;
Motor.ApplyState(state, true);
_jumpConsumed = rd.JumpConsumed;
_doubleJumpConsumed = rd.DoubleJumpConsumed;
_anchoringTimer = rd.AnchoringTimer;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment