using Fusion; using UnityEngine; using Fusion.Addons.SimpleKCC; namespace Projectiles { /// /// Third-person PlayerAgent for Photon Fusion + SimpleKCC. /// - Camera-relative movement /// - Character rotation toward move/aim direction /// - Shoulder camera with wall-collision /// [DefaultExecutionOrder(-5)] [RequireComponent(typeof(Weapons), typeof(Health), typeof(SimpleKCC))] public class PlayerAgent : ContextBehaviour { // --- Networked / components ------------------------------------------------ [Networked] public Player Owner { get; set; } public Weapons Weapons { get; private set; } public Health Health { get; private set; } public SimpleKCC KCC { get; private set; } public PlayerInput Input { get; private set; } public bool InputBlocked => Health.IsAlive == false; // --- Camera rig (reuse your existing references) --------------------------- [SerializeField] private Transform _cameraPivot; // yaw/pitch anchor above character [SerializeField] private Transform _cameraHandle; // optional visual/weapon anchor [Header("Third-Person Camera")] [SerializeField] private float _minPitch = -40f; [SerializeField] private float _maxPitch = 70f; [SerializeField] private float _yawRotateSpeed = 720f; // deg/sec when snapping to aim [SerializeField] private float _cameraDistance = 3.5f; // default boom length [SerializeField] private float _cameraDistanceMin = 1.0f; [SerializeField] private float _cameraDistanceMax = 5.0f; [SerializeField] private Vector3 _shoulderOffset = new Vector3(0.4f, 1.6f, 0f); [SerializeField] private LayerMask _cameraCollisionMask = ~0; // collide with world (exclude Player layer) [SerializeField] private float _cameraCollisionRadius = 0.15f; [SerializeField] private float _cameraDistanceSmoothing = 10f; // smooth camera distance changes // --- Movement -------------------------------------------------------------- [Header("Movement")] [SerializeField] private float _moveSpeed = 6f; [SerializeField] public float _upGravity = 15f; [SerializeField] public float _downGravity = 25f; [SerializeField] private float _jumpImpulse = 6f; [SerializeField] public float _groundAcceleration = 55f; [SerializeField] public float _groundDeceleration = 25f; [SerializeField] public float _airAcceleration = 25f; [SerializeField] public float _airDeceleration = 1.3f; [Header("Character Facing")] [SerializeField] private float _turnSpeed = 720f; // deg/sec turning toward desired facing [SerializeField] private bool _faceMoveDirection = true; // if false, faces camera forward (aiming) [Networked] private Vector3 _moveVelocity { get; set; } // Smooth camera distance for collision response private float _currentCameraDistance; // Local cache of most recent FUN (FixedUpdateNetwork) look to build on in Render/LateUpdate private Vector2 _lastFUNLookRotation; // (pitch,yaw) from KCC // --- Fusion lifecycle ------------------------------------------------------ public override void Spawned() { name = Object.InputAuthority.ToString(); // Only local player needs networked props replicated (bandwidth saver) ReplicateToAll(false); ReplicateTo(Object.InputAuthority, true); } public override void Despawned(NetworkRunner runner, bool hasState) { Owner = null; } public override void FixedUpdateNetwork() { if (Owner != null && Health.IsAlive) { ProcessMovementInput(); } // REMOVED: Don't set camera pivot here - it conflicts with LateUpdate // Store the look rotation for LateUpdate to use _lastFUNLookRotation = KCC.GetLookRotation(); } // --- Unity lifecycle ------------------------------------------------------- private void Awake() { KCC = GetComponent(); Weapons = GetComponent(); Health = GetComponent(); Input = GetComponent(); // Initialize camera distance _currentCameraDistance = _cameraDistance; } private void LateUpdate() { // 1) UPDATE LOOK ROTATION (LOCAL ONLY): accumulate mouse/controller deltas if (HasInputAuthority && Owner != null && Health.IsAlive) { // Accumulate pitch/yaw with clamp on pitch only; yaw is free var look = _lastFUNLookRotation + Input.AccumulatedLook; look.x = Mathf.Clamp(look.x, _minPitch, _maxPitch); KCC.SetLookRotation(look, _minPitch, _maxPitch); } // 2) Apply current pitch to pivot for everyone (affects remote weapon aim too) var currentLook = KCC.GetLookRotation(true, true); // Get both pitch and yaw _cameraPivot.localRotation = Quaternion.Euler(currentLook.x, 0f, 0f); // 3) Position the actual camera (LOCAL ONLY) with shoulder offset + collision if (HasInputAuthority) { var cam = Context.Camera.transform; // Desired camera target position (pivot in character space + shoulder offset) Vector3 pivotWorld = _cameraPivot.TransformPoint(_shoulderOffset); // Camera wants to sit behind pivot along its backward vector Quaternion yawWorld = Quaternion.Euler(0f, currentLook.y, 0f); Vector3 desiredOffset = yawWorld * new Vector3(0f, 0f, -_cameraDistance); Vector3 desiredPos = pivotWorld + desiredOffset; // Simple collision test: spherecast from pivot toward desired Vector3 dir = (desiredPos - pivotWorld); float len = dir.magnitude; Vector3 finalPos = desiredPos; float targetDistance = _cameraDistance; if (len > 0.0001f) { dir /= len; if (Physics.SphereCast(pivotWorld, _cameraCollisionRadius, dir, out RaycastHit hit, len, _cameraCollisionMask, QueryTriggerInteraction.Ignore)) { targetDistance = Mathf.Max(hit.distance - 0.05f, _cameraDistanceMin); } } // Smooth the camera distance changes for less jarring collision response _currentCameraDistance = Mathf.Lerp(_currentCameraDistance, targetDistance, _cameraDistanceSmoothing * Time.deltaTime); // Clamp within min/max boom and apply smoothed distance _currentCameraDistance = Mathf.Clamp(_currentCameraDistance, _cameraDistanceMin, _cameraDistanceMax); finalPos = pivotWorld + dir * _currentCameraDistance; cam.position = finalPos; cam.rotation = Quaternion.Euler(currentLook.x, currentLook.y, 0f); // Keep your optional handle aligned to the camera (muzzle/crosshair visuals) if (_cameraHandle != null) { _cameraHandle.position = cam.position; _cameraHandle.rotation = cam.rotation; } } } // --- Input → Movement / Facing -------------------------------------------- private void ProcessMovementInput() { if (!GetInput(out GameplayInput input)) return; // 1) Update look from input deltas (affects yaw for camera-relative movement) KCC.AddLookRotation(input.LookRotationDelta, _minPitch, _maxPitch); // 2) Gravity preference: faster fall feels better KCC.SetGravity(KCC.RealVelocity.y >= 0f ? _upGravity : _downGravity); // 3) CAMERA-RELATIVE MOVE on XZ plane // Build forward/right from current yaw only (ignore pitch) float yaw = KCC.GetLookRotation(false, true).y; Quaternion yawRot = Quaternion.Euler(0f, yaw, 0f); Vector3 camForward = yawRot * Vector3.forward; Vector3 camRight = yawRot * Vector3.right; Vector3 inputDir = (camForward * input.MoveDirection.y + camRight * input.MoveDirection.x); inputDir.y = 0f; if (inputDir.sqrMagnitude > 1f) inputDir.Normalize(); // Desired velocity and acceleration/deceleration Vector3 desiredVel = inputDir * _moveSpeed; float accel = 1f; if (desiredVel == Vector3.zero) accel = KCC.IsGrounded ? _groundDeceleration : _airDeceleration; else accel = KCC.IsGrounded ? _groundAcceleration : _airAcceleration; _moveVelocity = Vector3.Lerp(_moveVelocity, desiredVel, accel * Runner.DeltaTime); // 4) Character facing: toward move dir OR toward camera forward (aim) Vector3 faceDir = _faceMoveDirection ? (_moveVelocity.sqrMagnitude > 0.01f ? _moveVelocity : (yawRot * Vector3.forward)) : (yawRot * Vector3.forward); faceDir.y = 0f; if (faceDir.sqrMagnitude > 0.0001f) { Quaternion target = Quaternion.LookRotation(faceDir, Vector3.up); // Use KCC for body yaw rotation, but only set the Y (yaw) component // This keeps the body rotation networked via KCC Vector2 currentLook = KCC.GetLookRotation(); float targetYaw = target.eulerAngles.y; float newYaw = Mathf.MoveTowardsAngle(currentLook.y, targetYaw, _turnSpeed * Runner.DeltaTime); KCC.SetLookRotation(new Vector2(currentLook.x, newYaw), _minPitch, _maxPitch); } // 5) Jump float jumpImpulse = (input.Buttons.WasPressed(Input.PreviousButtons, EInputButton.Jump) && KCC.IsGrounded) ? _jumpImpulse : 0f; // 6) Move KCC.Move(_moveVelocity, jumpImpulse); } } }