Problem with syncing first person camera
Options
Hello, I'm new in creating multiplayer games, I'm following the 'Advanced Tutorial' from documentation, but Im trying to create it as first person shooter, right now player can connect, their position and rotation is synced, so client's see that someone's rotating on the horizontal axis, i made also vertical movement of camera, and it works for server and clients, but the thing is that clients don't see server rotating camera verticaly, im trying to solve this problem for few hours and nothing comes to my mind. Anyone can help?
PlayerMotor.cs (added camera rotation vertical)
PlayerController.cs
Server perspective:
https://imgur.com/juPR8LP
Client perspective:
https://imgur.com/C7gdukV
PlayerMotor.cs (added camera rotation vertical)
using UnityEngine; using System.Linq; using System.Collections; using System.Collections.Generic; [RequireComponent(typeof(CharacterController))] public class PlayerMotor : MonoBehaviour { public struct State { public Vector3 position; public Vector3 velocity; public bool isGrounded; public int jumpFrames; } State _state; CharacterController _cc; [SerializeField] float skinWidth = 0.08f; [SerializeField] float gravityForce = -9.81f; [SerializeField] float jumpForce = +40f; [SerializeField] int jumpTotalFrames = 30; [SerializeField] float movingSpeed = 4f; [SerializeField] float maxVelocity = 32f; [SerializeField] Vector3 drag = new Vector3(1f, 0f, 1f); public LayerMask layerMask; Vector3 sphere { get { Vector3 p; p = transform.position; p.y += _cc.radius; p.y -= (skinWidth * 2); return p; } } Vector3 waist { get { Vector3 p; p = transform.position; p.y += _cc.height / 2f; return p; } } public bool jumpStartedThisFrame { get { return _state.jumpFrames == (jumpTotalFrames - 1); } } void Awake() { _cc = GetComponent<CharacterController>(); _state = new State(); _state.position = transform.localPosition; } public void SetState(Vector3 position, Vector3 velocity, bool isGrounded, int jumpFrames) { // assign new state _state.position = position; _state.velocity = velocity; _state.jumpFrames = jumpFrames; _state.isGrounded = isGrounded; // assign local position _cc.Move(_state.position - transform.localPosition); } void Move(Vector3 velocity) { bool isGrounded = false; isGrounded = isGrounded || _cc.Move(velocity * BoltNetwork.FrameDeltaTime) == CollisionFlags.Below; isGrounded = isGrounded || _cc.isGrounded; isGrounded = isGrounded || Physics.CheckSphere(sphere, _cc.radius, layerMask); if (isGrounded && !_state.isGrounded) { _state.velocity = new Vector3(); } _state.isGrounded = isGrounded; _state.position = transform.localPosition; } public State Move(bool forward, bool backward, bool left, bool right, bool jump, float yaw, float pitch) { var moving = false; var movingDir = Vector3.zero; if (forward ^ backward) { movingDir.z = forward ? +1 : -1; } if (left ^ right) { movingDir.x = right ? +1 : -1; } if (movingDir.x != 0 || movingDir.z != 0) { moving = true; movingDir = Vector3.Normalize(Quaternion.Euler(0, yaw, 0) * movingDir); } // if (_state.isGrounded) { if (jump && _state.jumpFrames == 0) { _state.jumpFrames = (byte)jumpTotalFrames; _state.velocity += movingDir * movingSpeed; } if (moving && _state.jumpFrames == 0) { Move(movingDir * movingSpeed); } } else { _state.velocity.y += gravityForce * BoltNetwork.FrameDeltaTime; } if (_state.jumpFrames > 0) { // calculate force float force; force = (float)_state.jumpFrames / (float)jumpTotalFrames; force = jumpForce * force; Move(new Vector3(0, force, 0)); } // decrease jump frames _state.jumpFrames = Mathf.Max(0, _state.jumpFrames - 1); // clamp velocity _state.velocity = Vector3.ClampMagnitude(_state.velocity, maxVelocity); // apply drag _state.velocity.x = ApplyDrag(_state.velocity.x, drag.x); _state.velocity.y = ApplyDrag(_state.velocity.y, drag.y); _state.velocity.z = ApplyDrag(_state.velocity.z, drag.z); // this might seem weird, but it actually gets around a ton of issues - we basically apply // gravity on the Y axis on every frame to simulate instant gravity if you step over a ledge _state.velocity.y = Mathf.Min(_state.velocity.y, gravityForce); // apply movement Move(_state.velocity); // set local rotation transform.localRotation = Quaternion.Euler(0, yaw, 0); //BoltLog.Error($"{transform.name} {(int)pitch}"); transform.GetChild(0).localRotation = Quaternion.Euler(pitch, 0, 0); // detect tunneling DetectTunneling(); // update position _state.position = transform.localPosition; // done return _state; } float ApplyDrag(float value, float drag) { if (value < 0) { return Mathf.Min(value + (drag * BoltNetwork.FrameDeltaTime), 0f); } else if (value > 0) { return Mathf.Max(value - (drag * BoltNetwork.FrameDeltaTime), 0f); } return value; } void DetectTunneling() { RaycastHit hit; if (Physics.Raycast(waist, Vector3.down, out hit, _cc.height / 2, layerMask)) { transform.position = hit.point; } } void OnDrawGizmos() { if (Application.isPlaying) { Gizmos.color = _state.isGrounded ? Color.green : Color.red; Gizmos.DrawWireSphere(sphere, _cc.radius); Gizmos.color = Color.magenta; Gizmos.DrawLine(waist, waist + new Vector3(0, -(_cc.height / 2f), 0)); } } }
PlayerController.cs
using Bolt; using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : Bolt.EntityBehaviour<I_PlayerState> { [SerializeField] Weapon[] weapons; [Space] public float mouseSensitivity = 10; AudioSource _weaponSfxSource; int weapon; PlayerMotor _motor; bool _forward; bool _backward; bool _left; bool _right; bool _jump; bool _fire; float _yaw; public float _pitch; private void Awake() { Cursor.visible = false; Cursor.lockState = CursorLockMode.Locked; _weaponSfxSource = GetComponent<AudioSource>(); //cc = GetComponent<CharacterController>(); } public override void Attached() { state.SetTransforms(state.Transform, transform); _motor = GetComponent<PlayerMotor>(); state.OnFire = () => { weapons[0].DisplayEffects(entity); }; } private void Update() { HandleMovement(true); } void HandleMovement(bool mouse) { _forward = Input.GetKey(KeyCode.W); _backward = Input.GetKey(KeyCode.S); _left = Input.GetKey(KeyCode.A); _right = Input.GetKey(KeyCode.D); _jump = Input.GetKeyDown(KeyCode.Space); _fire = Input.GetMouseButton(0); if (mouse) { _yaw += (Input.GetAxisRaw("Mouse X") * mouseSensitivity * 10 * Time.deltaTime); _yaw %= 360f; _pitch += (-Input.GetAxisRaw("Mouse Y") * mouseSensitivity * 10 * Time.deltaTime); _pitch = Mathf.Clamp(_pitch, -85f, +85f); } } public override void SimulateController() { HandleMovement(false); if (!GetComponentInChildren<Camera>().enabled) GetComponentInChildren<Camera>().enabled = true; IPlayerCommandInput input = PlayerCommand.Create(); input.Forward = _forward; input.Backward = _backward; input.Left = _left; input.Right = _right; input.Jump = _jump; input.Yaw = _yaw; input.Pitch = _pitch; input.Fire = _fire; entity.QueueInput(input); } public override void ExecuteCommand(Command command, bool resetState) { PlayerCommand cmd = (PlayerCommand)command; if (resetState) { _motor.SetState(cmd.Result.Position, cmd.Result.Velocity, cmd.Result.IsGrounded, cmd.Result.JumpFrames); } else { // apply movement (this runs on both server and client) PlayerMotor.State motorState = _motor.Move(cmd.Input.Forward, cmd.Input.Backward, cmd.Input.Left, cmd.Input.Right, cmd.Input.Jump, cmd.Input.Yaw, cmd.Input.Pitch); // copy the motor state to the commands result (this gets sent back to the client) cmd.Result.Position = motorState.position; cmd.Result.Velocity = motorState.velocity; cmd.Result.IsGrounded = motorState.isGrounded; cmd.Result.JumpFrames = motorState.jumpFrames; if (cmd.IsFirstExecution) { //AnimatePlayer(cmd); state.Pitch = cmd.Input.Pitch; if (cmd.Input.Fire) { FireWeapon(cmd); } } } } void FireWeapon(PlayerCommand cmd) { if (weapons[0].FireFrame + weapons[0].FireInterval <= BoltNetwork.ServerFrame) { weapons[0].FireFrame = BoltNetwork.ServerFrame; state.Fire(); } } }
Server perspective:
https://imgur.com/juPR8LP
Client perspective:
https://imgur.com/C7gdukV
0
Comments
-
Hello @dawidos2221,
On your implementation, you are changing the pitch of the weapon/camera on the "ExecuteCommand", this method will be executed only on the "Owner" and the "Controller" of the entity, meaning that on the server (that is the owner of both entities), it will work just fine, on the client (that is the controller of one of the entities), you will also see the update happening for that particular controlled entity, but not for the entity owned and controlled by the game server.
On the original "Advanced Tutorial" sample, the weapon/camera pitch is managed by reading the Entity State on the "PlayerCamera" script and updating the rotation accordingly. That is not by mistake, but it's necessary, as you are not changing the transform the of the entity itself, but rather of another game object transform based on its state properties.
You can use for this case, and extend your script, the State properties callbacks, that are invoked every time you change a particular property of your entity. Please take a look here: https://doc.photonengine.com/en-us/bolt/current/gameplay/state#state_callbacks.
Also, take a look at this page where we describe who is who on the Bolt context: https://doc.photonengine.com/en-us/bolt/current/reference/glossary.
And here, about the Command usage: https://doc.photonengine.com/en-us/bolt/current/gameplay/commands.0