239 lines
9.9 KiB
C#

using Fusion;
using UnityEngine;
using Fusion.Addons.SimpleKCC;
namespace Projectiles
{
/// <summary>
/// Third-person PlayerAgent for Photon Fusion + SimpleKCC.
/// - Camera-relative movement
/// - Character rotation toward move/aim direction
/// - Shoulder camera with wall-collision
/// </summary>
[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;
private Animator _anim;
private NetworkMecanimAnimator _netAnim;
private static readonly int HashMoveX = Animator.StringToHash("InputX"); // or "MoveX"
private static readonly int HashMoveY = Animator.StringToHash("InputY"); // or "MoveY"
[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);
_anim = GetComponentInChildren<Animator>(); // your mesh/visual Animator
_netAnim = GetComponent<NetworkMecanimAnimator>();
}
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<SimpleKCC>();
Weapons = GetComponent<Weapons>();
Health = GetComponent<Health>();
Input = GetComponent<PlayerInput>();
// 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();
float moveX = Vector3.Dot(inputDir, camRight);
float moveY = Vector3.Dot(inputDir, camForward);
const float damp = 0.1f; // seconds to smooth toward target
_anim.SetFloat(HashMoveX, moveX, damp, Time.deltaTime);
_anim.SetFloat(HashMoveY, moveY, damp, Time.deltaTime);
// 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);
}
}
}