I’m making a playermovement script for the character controller, I’ve been able to get everything working without too many issues. But after I jump I get pulled back to the original start position to then be slided to the end position of the jump. This only happens on the players client, the server just sees the jump happen as it should.
FishNetworking Version:
4.3.8R
Example:
My Script:
using FishNet.Object;
using FishNet.Object.Prediction;
using FishNet.Transporting;
using UnityEngine;
using UnityEngine.InputSystem;
public sealed class PlayerMovement : NetworkBehaviour
{
[SerializeField] private InputActionAsset inputActionAsset;
private InputAction _moveAction;
private InputAction _jumpAction;
private InputAction _turnLeftAction;
private InputAction _turnRightAction;
private InputAction _rightMouseAction;
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float backwardSpeed = 3f;
[SerializeField] private float jumpForce = 2f;
[SerializeField] private float turnSpeed = 2f;
[SerializeField] private float gravity = -15f;
[SerializeField] private float groundedOffset = -0.14f;
[SerializeField] private float groundedRadius = 0.28f;
[SerializeField] private LayerMask groundLayers;
private CharacterController _characterController;
private CameraController _cameraController;
private Vector3 _velocity;
private Vector3 _jumpDirection;
private bool _isGrounded;
private bool _rightMousePressed;
private bool _leftMousePressed;
private bool _middleMousePressed;
private bool _isMoving;
private bool _isAutoRunning = false;
private bool _isJumping;
public bool IsMoving => _isMoving;
public override void OnStartNetwork()
{
base.OnStartNetwork();
_characterController = GetComponent<CharacterController>();
TimeManager.OnTick += TimeManager_OnTick;
TimeManager.OnPostTick += TimeManager_OnPostTick;
}
public override void OnStartClient()
{
base.OnStartClient();
if (IsOwner)
{
_moveAction = inputActionAsset.FindActionMap("Player").FindAction("Move");
_jumpAction = inputActionAsset.FindActionMap("Player").FindAction("Jump");
_turnLeftAction = inputActionAsset.FindActionMap("Player").FindAction("Turn Left");
_turnRightAction = inputActionAsset.FindActionMap("Player").FindAction("Turn Right");
_rightMouseAction = inputActionAsset.FindActionMap("Player").FindAction("Right Mouse Camera Control");
_moveAction.Enable();
_jumpAction.Enable();
_turnLeftAction.Enable();
_turnRightAction.Enable();
_rightMouseAction.Enable();
_cameraController = FindFirstObjectByType<CameraController>();
}
}
public override void OnStopNetwork()
{
base.OnStopNetwork();
TimeManager.OnTick -= TimeManager_OnTick;
TimeManager.OnPostTick -= TimeManager_OnPostTick;
}
public override void OnStopClient()
{
base.OnStopClient();
if (IsOwner)
{
// Disable input actions for the owner when stopping
_moveAction?.Disable();
_jumpAction?.Disable();
_turnLeftAction?.Disable();
_turnRightAction?.Disable();
_rightMouseAction?.Disable();
}
}
private ReplicateData CreateReplicateData()
{
if (!base.IsOwner)
return default;
_rightMousePressed = _cameraController.isRightMousePressed;
_leftMousePressed = _cameraController.isLeftMousePressed;
_middleMousePressed = _cameraController.isMiddleMousePressed;
_isAutoRunning = (_leftMousePressed && _rightMousePressed) || _middleMousePressed;
_isMoving = _moveAction.ReadValue<Vector2>().magnitude > 0 || _isAutoRunning;
ReplicateData replicateData;
Vector2 combinedInput = _moveAction.ReadValue<Vector2>();
if (_isAutoRunning)
{
if (combinedInput.y < 0)
{
combinedInput = new Vector2(combinedInput.x, 0f);
}
else
{
combinedInput = new Vector2(combinedInput.x, 1f);
}
replicateData = new ReplicateData(
combinedInput,
_jumpAction.ReadValue<float>() > 0,
_turnLeftAction.ReadValue<float>() > 0,
_turnRightAction.ReadValue<float>() > 0,
true,
_cameraController.Yaw
);
}
else
{
replicateData = new ReplicateData(
_moveAction.ReadValue<Vector2>(),
_jumpAction.ReadValue<float>() > 0,
_turnLeftAction.ReadValue<float>() > 0,
_turnRightAction.ReadValue<float>() > 0,
_rightMousePressed,
_cameraController.Yaw
);
}
return replicateData;
}
private void TimeManager_OnTick()
{
Replicate(CreateReplicateData());
}
[Replicate]
private void Replicate(ReplicateData replicateData, ReplicateState replicateState = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
if (_characterController == null || !_characterController.enabled)
{
Debug.LogWarning("CharacterController is not active.");
return;
}
float delta = (float)TimeManager.TickDelta;
GroundCheck();
// Calculate movement speed
float currentMoveSpeed = moveSpeed;
if (replicateData.MovementInput.y < 0)
{
currentMoveSpeed = backwardSpeed;
}
Vector3 combinedMovement = Vector3.zero;
// Update player rotation based on camera yaw only if the right mouse button is pressed
if (replicateData.RightMousePressed || _isAutoRunning)
{
// Set the character's yaw to instantly match the camera's yaw
_characterController.transform.rotation = Quaternion.Euler(0f, replicateData.Yaw, 0f);
if (replicateData.TurnLeftInput)
{
// Strafe to the left
combinedMovement -= _characterController.transform.right;
}
if (replicateData.TurnRightInput)
{
// Strafe to the right
combinedMovement += _characterController.transform.right;
}
}
else
{
// Handle turning when the right mouse button is not pressed
if (replicateData.TurnLeftInput)
{
transform.Rotate(Vector3.up, -turnSpeed * delta);
}
if (replicateData.TurnRightInput)
{
transform.Rotate(Vector3.up, turnSpeed * delta);
}
}
if (!_isJumping)
{
// Calculate movement
combinedMovement += transform.right * replicateData.MovementInput.x + transform.forward * replicateData.MovementInput.y;
if (combinedMovement.magnitude > 1f)
{
combinedMovement.Normalize();
}
}
else
{
// Ensure _jumpDirection is always valid when jumping
combinedMovement = _jumpDirection;
}
if (replicateData.JumpInput && _isGrounded && !_isJumping)
{
_isJumping = true;
_jumpDirection = combinedMovement.normalized;
_velocity.y = Mathf.Sqrt(jumpForce * -2f * gravity);
}
// Apply gravity
if (!_isGrounded)
{
_velocity.y += gravity * delta;
}
_characterController.Move(currentMoveSpeed * delta * combinedMovement);
_characterController.Move(_velocity * delta);
// If we're the server, send reconciliation data back to the client
if (IsServerInitialized)
{
ReconciliationData reconcileData = new ReconciliationData(
_characterController.transform.position,
_characterController.transform.rotation,
_velocity,
Vector3.zero, // CharacterController doesn't use angular velocity
_isJumping
);
Reconcile(reconcileData, channel);
}
}
private void GroundCheck()
{
Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - groundedOffset, transform.position.z);
_isGrounded = Physics.CheckSphere(spherePosition, groundedRadius, groundLayers, QueryTriggerInteraction.Ignore);
if (_isGrounded)
{
// Reset vertical velocity when grounded
_velocity.y = -2f;
_isJumping = false; // Ensure jump is reset when grounded
}
}
private void TimeManager_OnPostTick()
{
if (IsServerInitialized)
{
CreateReconcile();
}
}
public override void CreateReconcile()
{
ReconciliationData reconcileData = new ReconciliationData(
_characterController.transform.position,
_characterController.transform.rotation,
_velocity,
Vector3.zero, // CharacterController doesn't use angular velocity
_isJumping
);
Reconcile(reconcileData);
}
[Reconcile]
private void Reconcile(ReconciliationData reconciliationData, Channel channel = Channel.Unreliable)
{
transform.SetPositionAndRotation(reconciliationData.Position, reconciliationData.Rotation);
_velocity = reconciliationData.Velocity;
}
private struct ReplicateData : IReplicateData
{
public readonly Vector2 MovementInput;
public readonly bool JumpInput;
public readonly bool TurnLeftInput;
public readonly bool TurnRightInput;
public readonly bool RightMousePressed;
public readonly float Yaw;
public ReplicateData(Vector2 movementInput, bool jumpInput, bool turnLeftInput, bool turnRightInput, bool rightMousePressed, float yaw) : this()
{
MovementInput = movementInput;
JumpInput = jumpInput;
TurnLeftInput = turnLeftInput;
TurnRightInput = turnRightInput;
RightMousePressed = rightMousePressed;
Yaw = yaw;
}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
private struct ReconciliationData : IReconcileData
{
public readonly Vector3 Position;
public readonly Quaternion Rotation;
public readonly Vector3 Velocity;
public readonly Vector3 AngularVelocity;
public readonly bool IsJumping;
public ReconciliationData(Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity, bool isJumping) : this()
{
Position = position;
Rotation = rotation;
Velocity = velocity;
AngularVelocity = angularVelocity;
IsJumping = isJumping;
}
private uint _tick;
public void Dispose() { }
public uint GetTick() => _tick;
public void SetTick(uint value) => _tick = value;
}
}