Hello,
I’m using Character Controller and State Machine to manage the movement and Animation of my player character. The rotation is coding directly without state machine.
I’m using the tutorial of CodeMonkey Youtube channel to implement multiplayer with Unity Network for GameObject but I encounter a really early issue with syncing the position.
I host a server via a build, let’s say Build01. I move the player in this instance. Then with a second build, Build02, I’m joiining the frist player. I see the actual position of the player1 but if I moving it now, the second player can’t see him moving. And The player1 can’t see the Player 2 moving.
both player can’t see the animation either. Each player can move in its instance but can’t see the other player moving.
BUT player 1 can see the rotation of player 2 and vice versa.
So I think the problem comes from Character Controller and/or my state machine.
The player is in grounded state + Idle Substate and then, if I press WSAD key or a LeftStick Gamepad, the player pass to Grounded State + Walk Substate.
It seems like the character Controller or the State machine don’t communicate the .move function to the Network or receive a zéro vector in the .move function.
Can someone please help me ?
In the “Client Netwtok Transform” component, I’ve checked sync position in x, y and Z. Same with Rotation.
Base State :
using UnityEngine;
public abstract class CharacterBaseState
{
private bool _isRootState = false; //Variable to tell if the concrete state is at the top of the state hierarchy
private CharacterStateMachine _ctx;
private CharacterStateFactory _factory;
private CharacterBaseState _currentSubState;
private CharacterBaseState _currentSuperState;
protected bool IsRootState { set { _isRootState = value; } }
protected CharacterStateMachine Ctx { get { return _ctx; } }
protected CharacterStateFactory Factory { get { return _factory; } }
public CharacterBaseState(CharacterStateMachine currentContext, CharacterStateFactory characterStateFactory)
{
_ctx = currentContext;
_factory = characterStateFactory;
}
public abstract void EnterState();
public abstract void UpdateState();
public abstract void ExitState();
public abstract void CheckSwitchState();
public abstract void InitializeSubState();
public void UpdateStates()
{
UpdateState();
if (_currentSubState != null)
{
_currentSubState.UpdateStates();
}
}
protected void SwitchState(CharacterBaseState newState)
{
//First we have to exit the current State
ExitState();
//Then we can "Enter" in a new state
newState.EnterState();
if (_isRootState) //We only switch tge context's current state if it's at the top level of the state chain
{
//Finally the context set the "_currentState" to be the newState
_ctx.CurrentState = newState;
}
else if (_currentSuperState != null)
{
_currentSuperState.SetSubState(newState);
}
}
protected void SetSuperState(CharacterBaseState newSuperState)
{
_currentSuperState = newSuperState;
}
protected void SetSubState(CharacterBaseState newSubState)
{
_currentSubState = newSubState;
newSubState.SetSuperState(this);
}
}
Walk State :
using UnityEngine;
public class CharacterWalkingState : CharacterBaseState
{
//Vector use to use the right animation regarding the direction where the player is looking at and the direction input
Vector3 VelocityAnimation;
public CharacterWalkingState(CharacterStateMachine currentContext, CharacterStateFactory characterStateFactory)
: base(currentContext, characterStateFactory) { }
//Increase performance
int VelocityXHash;
int VelocityZHash;
public override void EnterState()
{
////Debug.Log("WELCOME TO THE WALK SUBSTATE2");
Ctx.Animator.SetBool(Ctx.IsWalkingHash, true);
VelocityXHash = Animator.StringToHash("Velocity X");
VelocityZHash = Animator.StringToHash("Velocity Z");
}
public override void UpdateState()
{
//Movement applied to the character
Ctx.AppliedMovementX = (Ctx.CurrentMovement.x * (Mathf.Cos(Ctx.CameraRotation * Mathf.PI / 180))) - (Ctx.CurrentMovement.y * (Mathf.Sin(Ctx.CameraRotation * Mathf.PI / 180)));
Ctx.AppliedMovementZ = (Ctx.CurrentMovement.y * (Mathf.Cos(Ctx.CameraRotation * Mathf.PI / 180))) + (Ctx.CurrentMovement.x * (Mathf.Sin(Ctx.CameraRotation * Mathf.PI / 180)));
float _zDirzChara = Vector3.Angle(new Vector3(0,0,Ctx.AppliedMovementZ), Ctx.TransformForwardChara); //Angle between the Z input and the forward vector of the character
////Debug.Log(" Angle _zDirzChara " + _zDirzChara);
float _xDirzChara = Vector3.Angle(new Vector3(Ctx.AppliedMovementX, 0, 0), Ctx.TransformForwardChara); //Angle between the X input and the forward vector of the character
////Debug.Log(" Angle _xDirzChara " + _xDirzChara);
float _xDirxChara = Vector3.Angle(new Vector3(Ctx.AppliedMovementX, 0, 0), Ctx.TransformRightChara); //Angle between the X input and the right vector of the character
////Debug.Log(" Angle _xDirxChara " + _xDirxChara);
float _zDirxChara = Vector3.Angle(new Vector3(0, 0, Ctx.AppliedMovementZ), Ctx.TransformRightChara); //Angle between the Z input and the right vector of the character
/*//Debug.Log(" Angle _zDirxChara " + _zDirxChara);
//Debug.Log("Angle Ctx.AppliedMovementX : " + Ctx.AppliedMovementX);
//Debug.Log("Angle Ctx.AppliedMovementZ : " + Ctx.AppliedMovementZ);
//Debug.Log("Angle Ctx.AppliedMovementX * Mathf.Cos(_xDirxChara) : " + Ctx.AppliedMovementX * Mathf.Cos(_xDirxChara * Mathf.PI / 180));
//Debug.Log("Angle Ctx.AppliedMovementZ * Mathf.Cos(_zDirxChara) : " + Ctx.AppliedMovementZ * Mathf.Cos(_zDirxChara * Mathf.PI / 180));
//Debug.Log("Angle Ctx.AppliedMovementZ * Mathf.Cos(_zDirzChara) : " + Ctx.AppliedMovementZ * Mathf.Cos(_zDirzChara * Mathf.PI / 180));
//Debug.Log("Angle Ctx.AppliedMovementX * Mathf.Cos(_xDirzChara) : " + Ctx.AppliedMovementX * Mathf.Cos(_xDirzChara * Mathf.PI / 180));
*/
//Result on the animation of the direction Input and the direction where the character is looking at
VelocityAnimation.x = Mathf.Abs(Ctx.AppliedMovementX) * Mathf.Cos(_xDirxChara * Mathf.PI / 180) + Mathf.Abs(Ctx.AppliedMovementZ) * Mathf.Cos(_zDirxChara * Mathf.PI / 180);
VelocityAnimation.y = 0;
VelocityAnimation.z = Mathf.Abs(Ctx.AppliedMovementZ) * Mathf.Cos(_zDirzChara * Mathf.PI / 180) + Mathf.Abs(Ctx.AppliedMovementX) * Mathf.Cos(_xDirzChara * Mathf.PI / 180);
Vector3.Normalize(VelocityAnimation);
Ctx.Animator.SetFloat(VelocityXHash, VelocityAnimation.x);
Ctx.Animator.SetFloat(VelocityZHash, VelocityAnimation.z);
CheckSwitchState();
}
public override void ExitState() { }
public override void CheckSwitchState()
{
if(!Ctx.IsMovementPressed)
{
SwitchState(Factory.Idle());
}
}
public override void InitializeSubState() { }
}
And finally the Character State Machine (Where the .Move() function is called )
using Unity.Netcode;
using Cinemachine;
using UnityEngine;
using UnityEngine.InputSystem;
//In the State Machine, this script is the "Context"
public class CharacterStateMachine : NetworkBehaviour
{
//Declare reference variables
PlayerInput _playerInput;
CharacterController _charaController;
Animator _animator;
//Variables to store optimized setter/getter parameter Ids
int _isMovingHash;
//Variables to store player input values
Vector2 _currentMovementInput = new Vector2(0, 0);
Vector3 _currentMovement = new Vector3(0, 0, 0);
Vector3 _appliedMovement = new Vector3(0, 0, 0);
bool _isMovementPressed = false;
//Movement variables
public float _speed;
public float _rotationSpeed = 15.0f;
//Third person variable
Vector3 _cameraRelatioveMovement;
Quaternion lookRotation;
Vector3 _rotationRelativeMovement;
//Gravity variables
//public float _groundedGravity = -0.05f;
public float _gravity = -9.81f;
//Jump variables
bool _isJumpPressed = false;
float _initialJumpVelocity;
public float _maxJumpHeight = 0.75f;
public float _maxJumpTime = 2f;
bool _isJumping = false;
int _isJumpingHash;
int _isFallingHash;
//bool _isJumpingAnimating = false;//No more unnecessary variables from the previous implementation, without the State one.
bool _requireNewJumpPress;
//State variables
[SerializeField]
CharacterBaseState _currentState;
CharacterStateFactory _states;
/*
public bool IsPickUpPressed { get { return GetComponent<PickUpBox>().GetIsPickUpPressed(); } set { GetComponent<PickUpBox>().SetIsPickUpPressed(value); } }
public bool IsHoldingBox { get { return GetComponent<PickUpBox>().GetIsHoldingBox(); } set { GetComponent<PickUpBox>().SetIsHoldingBox(value); } }
public bool IsRequireNewPickUpPress { get { return GetComponent<PickUpBox>().GetRequireNewPickUpPress(); } set { GetComponent<PickUpBox>().SetRequireNewPickUpPress(value); } }
*/
//Camera variables
[SerializeField] private CinemachineVirtualCamera _cam;
bool requireNewCamRotationRightPress = false; //to force player to unpress the camRotation button to press it again to really camRotation
bool requireNewCamRotationLeftPress = false; //to force player to unpress the camRotation button to press it again to really camRotation
bool _isCamRotationRightPressed;
bool _isCamRotationLeftPressed;
float _cameraRotation;
Vector2 mousePosition;
public float CameraRotation { get { return _cameraRotation; } }
bool _isLockRotToMove;
bool requireNewLockRotToMove = true;
//Gamepad variables
public bool isGamepad;
Vector2 aimGamepad;
Vector2 aim;
float gamepadRotateRatio = 1000f;
bool _isLookWithRightStick;
//Knock variables
float _knockForce;
Vector3 _knockVelocity;
Vector3 _dirOfShoot;
float _knockTime;
//Recoil Impact
float _recoilImpactForce;
Vector3 _recoilImpactVelocity;
Vector3 _dirOfRecoilImpact;
float _recoilImpactTime;
//Getter and Setter of the State variables
public CharacterBaseState CurrentState { get { return _currentState; } set { _currentState = value; } }
public bool IsJumpPressed { get { return _isJumpPressed; } }
public Animator Animator { get { return _animator; } }
public bool IsMovementPressed { get { return _isMovementPressed; } }
public int IsWalkingHash { get { return _isMovingHash; } set { _isMovingHash = value; } }
public float AppliedMovementX { get { return _appliedMovement.x; } set { _appliedMovement.x = value; } }
public float AppliedMovementZ { get { return _appliedMovement.z; } set { _appliedMovement.z = value; } }
public Vector2 CurrentMovement { get {return _currentMovementInput; } set { _currentMovementInput = value; } }
public bool IsJumping { set {_isJumping = value; }}
public int IsJumpingHash { get { return _isJumpingHash; } }
public float InitialJumpVelocity { get { return _initialJumpVelocity; } }
public int IsFallingHash { get { return _isFallingHash; } set { _isFallingHash = value; } }
public float CurrentMovementY { get { return _currentMovement.y; } set { _currentMovement.y = value; } }
public float AppliedMovementY { get { return _appliedMovement.y; } set { _appliedMovement.y = value; } }
public Vector3 ApplyMovement { get { return _appliedMovement; } }
public float Gravity { get { return _gravity; } set { _gravity = value; } }
public bool RequireNewJumpPress { get { return _requireNewJumpPress; } set { _requireNewJumpPress = value; } }
public CharacterController CharaController { get { return _charaController; } set { _charaController = value; } }
public Quaternion Rotation { get { return transform.rotation; } }
public Vector3 TransformForwardChara { get { return transform.forward; } }
public Vector3 TransformRightChara { get { return transform.right; } }
public Vector3 TransformRelativeWorldToLocal { get { return transform.InverseTransformDirection(Vector3.forward); } }
public Vector3 TransformRelativeLocalToWorld { get { return transform.TransformDirection(transform.forward); } }
public Vector3 CameraRelativeMovement { get { return _cameraRelatioveMovement; } }
Vector3 _positionToLookAt;
void Awake()
{
//Inityally set reference variable
_playerInput = new PlayerInput();
_charaController = GetComponent<CharacterController>();
_animator = GetComponent<Animator>();
_speed = GetComponent<Stats_Character>().GetSpeed();
_cam = GetComponent<VirtualCameraAssign>().GetVC();
//Setup state
_states = new CharacterStateFactory(this);
_currentState = _states.Grounded();
_currentState.EnterState();
//Set the parameter hash references
_isMovingHash = Animator.StringToHash("isMoving");
_isJumpingHash = Animator.StringToHash("isJumping");
_isFallingHash = Animator.StringToHash("isFalling");
//Player Input
_playerInput.CharaControls.Move.started += OnMovementInput;
_playerInput.CharaControls.Move.canceled += OnMovementInput;
_playerInput.CharaControls.Move.performed += OnMovementInput;
_playerInput.CharaControls.Jump.started += OnJump;
_playerInput.CharaControls.Jump.canceled += OnJump;
_playerInput.CharaControls.CamRotationLeft.started += onCamRotationLeft;
_playerInput.CharaControls.CamRotationLeft.canceled += onCamRotationLeft;
_playerInput.CharaControls.CamRotationRight.started += onCamRotationRight;
_playerInput.CharaControls.CamRotationRight.canceled += onCamRotationRight;
_playerInput.CharaControls.Look.started += onRotationInput;
_playerInput.CharaControls.Look.canceled += onRotationInput;
_playerInput.CharaControls.Look.performed += onRotationInput;
_playerInput.CharaControls.LockRotToMove.started += onLockRotToMove;
_playerInput.CharaControls.LockRotToMove.canceled += onLockRotToMove;
SetupJumpVariables();
}
//Set the intial velocity and gravity to use jump height and duration
void SetupJumpVariables()
{
float timeToApex = _maxJumpTime / 2;
_gravity = (-2 * _maxJumpHeight) / Mathf.Pow(timeToApex, 2);
_initialJumpVelocity = (2 * _maxJumpHeight) / timeToApex;
}
// Start is called before the first frame update
void Start()
{
if (_speed <= 0)
{
_speed = GetComponent<Stats_Character>().GetSpeed();
}
}
// Update is called once per frame
void Update()
{
if (!IsOwner)
{
return;
}
if (_cam == null)
{
_cam = GetComponent<VirtualCameraAssign>().GetVC();
}
HandleRotation();
HandleMove();
_currentState.UpdateStates();
}
private void HandleMove()
{
if(_knockTime > 0) //When player shoot, the character receive a force that make him get backward
{
_knockTime -= Time.deltaTime;
_knockVelocity = _dirOfShoot * (_knockForce * _knockTime);
}
else if (_knockTime <= 0)
{
_knockVelocity = Vector3.zero;
}
if (_recoilImpactTime > 0) // When player receive damage, the character receive a force that get him go to the direction opposite of where the force come from
{
_recoilImpactTime -= Time.deltaTime;
_recoilImpactVelocity = _dirOfRecoilImpact * (_recoilImpactForce * _recoilImpactTime);
}
else if (_recoilImpactTime <= 0)
{
_recoilImpactVelocity = Vector3.zero;
}
_charaController.Move((_appliedMovement+_knockVelocity + _recoilImpactVelocity) * _speed * Time.deltaTime);
}
void HandleRotation()
{
//Vector3 _positionToLookAt;
//Verify if the player is playing with a Gamepad
if (isGamepad)
{
//Debug.Log("Mathf.Abs(aim.x) : " + Mathf.Abs(aim.x));
// /Debug.Log("Mathf.Abs(aim.y) : " + Mathf.Abs(aim.y));
//Debug.Log("controllerDeadzone : " + controllerDeadzone);
//Debug.Log("isLookWithRightStick : " + _isLookWithRightStick);
//If the player is orienting the right joystick, the character will look to the direction poiting by this joystick
if (_isLookWithRightStick)
{
aimGamepad.x = /*_isRunPressed ? */(aim.x) * (Mathf.Cos(_cameraRotation * Mathf.PI / 180)) - (aim.y) * (Mathf.Sin(_cameraRotation * Mathf.PI / 180));/* : _currentMovementInput.x;*/
aimGamepad.y = /*_isRunPressed ? */(aim.y) * (Mathf.Cos(_cameraRotation * Mathf.PI / 180)) + (aim.x) * (Mathf.Sin(_cameraRotation * Mathf.PI / 180));/* : _currentMovementInput.y;*/
Vector3 playerDirection = Vector3.right * aimGamepad.x + Vector3.forward * aimGamepad.y;
if (playerDirection.sqrMagnitude > 0.0f)
{
Quaternion newRotation = Quaternion.LookRotation(playerDirection, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, newRotation, gamepadRotateRatio * Time.deltaTime);
}
}
//If Player doesn't use the Joystick, the character will look in the direction of the movement
else if (!_isLookWithRightStick)
{
_positionToLookAt.x = (_currentMovementInput.x * (Mathf.Cos(_cameraRotation * Mathf.PI / 180))) - (_currentMovementInput.y * (Mathf.Sin(_cameraRotation * Mathf.PI / 180))); //_cameraRelatioveMovement.x instead of _currentMovementInput.x for a third person view
_positionToLookAt.y = 0.0f;
_positionToLookAt.z = (_currentMovementInput.y * (Mathf.Cos(_cameraRotation * Mathf.PI / 180))) + (_currentMovementInput.x * (Mathf.Sin(_cameraRotation * Mathf.PI / 180))); ; //_cameraRelatioveMovement.y instead of _currentMovementInput.y for a third person view
Quaternion _currentRotation = transform.rotation;
if (_isMovementPressed)
{
//Create a new rotation based on where the player is currently pressing
Quaternion _targetRotation = Quaternion.LookRotation(_positionToLookAt);
transform.rotation = Quaternion.Slerp(_currentRotation, _targetRotation, _rotationSpeed * Time.deltaTime);
}
}
}
//If player is using a keyboard
else if (!isGamepad)
{
//If RequireNewLockRotToMove is true the player will look to direction of the mouse
if (requireNewLockRotToMove)
{
Debug.Log("2 Cam : " + _cam.transform.name);
Ray ray = Camera.main.ScreenPointToRay(mousePosition);
if (Physics.Raycast(ray, out RaycastHit raycastHit, 100.0f, ~(1<<11)))
{
_positionToLookAt = raycastHit.point;
//_positionToLookAt.y = 1.5f;
LookAt(_positionToLookAt);
}
}
//If RequireNewLockRotToMove is false, the character will look in the direction of the movement
else if (!requireNewLockRotToMove)
{
Debug.Log("4 Cam : " + _cam.transform.name);
_positionToLookAt.x = (_currentMovementInput.x * (Mathf.Cos(_cameraRotation * Mathf.PI / 180))) - (_currentMovementInput.y * (Mathf.Sin(_cameraRotation * Mathf.PI / 180))); //_cameraRelatioveMovement.x instead of _currentMovementInput.x for a third person view
_positionToLookAt.y = 0.0f;
_positionToLookAt.z = (_currentMovementInput.y * (Mathf.Cos(_cameraRotation * Mathf.PI / 180))) + (_currentMovementInput.x * (Mathf.Sin(_cameraRotation * Mathf.PI / 180))); ; //_cameraRelatioveMovement.y instead of _currentMovementInput.y for a third person view
Quaternion _currentRotation = transform.rotation;
if (_isMovementPressed)
{
//Create a new rotation based on where the player is currently pressing
Quaternion _targetRotation = Quaternion.LookRotation(_positionToLookAt);
transform.rotation = Quaternion.Slerp(_currentRotation, _targetRotation, _rotationSpeed * Time.deltaTime);
}
}
//Debug.Log("Camera Rotation : " + _cameraRotation);
}
}
public void LookAt(Vector3 lookPoint) // Use to have the player looking where the mouse is
{
Vector3 direction = (lookPoint - transform.position);
lookRotation = Quaternion.LookRotation(new Vector3(direction.x, 0.0f, direction.z));
transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * _rotationSpeed);
}
void OnMovementInput(InputAction.CallbackContext context) //Use to detect the WSAD or leftstick input
{
_currentMovementInput = context.ReadValue<Vector2>();
_currentMovement.x = _currentMovementInput.x;
_currentMovement.z = _currentMovementInput.y;
_isMovementPressed = _currentMovementInput.x != 0 || _currentMovementInput.y != 0;
}
void OnJump(InputAction.CallbackContext context) //Use to detect jump button pressed
{
_isJumpPressed = context.ReadValueAsButton();
_requireNewJumpPress = false;
}
void onCamRotationRight(InputAction.CallbackContext context) //Rotate the camera around player anti-clockwise
{
_isCamRotationRightPressed = context.ReadValueAsButton();
requireNewCamRotationRightPress = false;
_cameraRotation = _cam.GetComponent<CameraControl>().GetCameraRotation();
}
void onCamRotationLeft(InputAction.CallbackContext context) //Rotate the Camera around player clockwise
{
_isCamRotationLeftPressed = context.ReadValueAsButton();
requireNewCamRotationLeftPress = false;
_cameraRotation = _cam.GetComponent<CameraControl>().GetCameraRotation();
}
void onRotationInput(InputAction.CallbackContext context) //If player use the right stick, the character will look ind the direction of the stick and if player don't use the right stick, the character will look in the direction of the movement
{
mousePosition = context.ReadValue<Vector2>();
aim = context.ReadValue<Vector2>();
if (context.performed && isGamepad)
{
_isLookWithRightStick = true;
}
else if (context.canceled && isGamepad)
{
_isLookWithRightStick = false;
}
}
void onLockRotToMove(InputAction.CallbackContext context)
{
_isLockRotToMove = context.ReadValueAsButton();
if (_isLockRotToMove && requireNewLockRotToMove)
{
////Debug.Log("Require new lock to move 1 : " + requireNewLockRotToMove);
requireNewLockRotToMove = false;
////Debug.Log("Require new lock to move 2 : " + requireNewLockRotToMove);
return;
}
else if (_isLockRotToMove && !requireNewLockRotToMove)
{
////Debug.Log("Require new lock to move 3 : " + requireNewLockRotToMove);
requireNewLockRotToMove = true;
////Debug.Log("Require new lock to move 4 : " + requireNewLockRotToMove);
return;
}
}
//Setting of knock due shooting
public void SetKnockVariables(float knockForce, float knocTime)
{
_knockForce = knockForce;
_knockTime = knocTime;
_dirOfShoot = -transform.forward;
}
public void SetImpactRecoil(float impactRecoilForce, float impactRecoilTime, Vector3 impactDir)
{
_recoilImpactForce = impactRecoilForce;
_recoilImpactTime = impactRecoilTime;
_dirOfRecoilImpact = impactDir;
}
void OnEnable()
{
//Enable the chara controls action map
_playerInput.CharaControls.Enable();
}
void OnDisable()
{
//Disable the chara controls action map
_playerInput.CharaControls.Disable();
}
}