using System.Collections.Generic; using UnityEngine; using Fusion; namespace OnlyScove.Scripts { [RequireComponent(typeof(CharacterController), typeof(InputReader), typeof(Animator))] public class PlayerStateMachine : NetworkBehaviour { [field: Header("References")] [field: SerializeField] public CharacterController Controller { get; private set; } [field: SerializeField] public virtual InputReader Input { get; private set; } [field: SerializeField] public Animator Anim { get; private set; } [field: SerializeField] public EnvironmentScanner Scanner { get; private set; } public CameraController Cam { get; private set; } [field: Header("Animator Settings")] [SerializeField] private string speedParamName = "Speed"; [SerializeField] private string velocityXParamName = "Velocity X"; [SerializeField] private string velocityZParamName = "Velocity Z"; private int speedHash; private int velocityXHash; private int velocityZHash; [field: Header("Movement Settings")] [field: SerializeField] public float WalkSpeed { get; private set; } = 3f; [field: SerializeField] public float RunSpeed { get; private set; } = 6f; [field: SerializeField] public float SprintSpeed { get; private set; } = 9f; [field: SerializeField] public float SneakSpeed { get; private set; } = 1.5f; [field: SerializeField] public float DashForce { get; private set; } = 10f; [field: SerializeField] public float RotationSpeed { get; private set; } = 500f; [field: SerializeField] public float AnimationDamping { get; private set; } = 0.2f; [field: Header("Airborne Settings")] [field: SerializeField] public float JumpHeight { get; private set; } = 2f; [field: SerializeField] public float Gravity { get; private set; } = -15f; [field: SerializeField] public float ThrustDownwardForce { get; private set; } = -20f; [field: Header("Ground Check")] [field: SerializeField] public float GroundCheckRadius { get; private set; } = 0.2f; [field: SerializeField] public Vector3 GroundCheckOffset { get; private set; } [field: SerializeField] public LayerMask GroundMask { get; private set; } [field: Header("Interaction")] [field: SerializeField] public float InteractionRange { get; private set; } = 2f; [field: SerializeField] public LayerMask InteractionMask { get; private set; } [Networked] public Quaternion NetworkedCameraRotation { get; set; } [Networked] public Vector2 NetworkedMoveInput { get; set; } [Networked] public float NetworkedSpeed { get; set; } [Networked] public Vector3 NetworkedPosition { get; set; } [Header("Player Stats")] [Networked, OnChangedRender(nameof(OnHealthChangedRender))] public float Health { get; set; } = 100f; [Networked, OnChangedRender(nameof(OnStaminaChangedRender))] public float Stamina { get; set; } = 100f; [Networked, OnChangedRender(nameof(OnNoiseLevelChangedRender))] public float NoiseLevel { get; set; } = 0f; public event System.Action OnHealthChanged; public event System.Action OnStaminaChanged; public event System.Action OnNoiseLevelChanged; public event System.Action OnInteractableTargetChanged; public Vector2 MoveInput { get; private set; } public bool IsSprintHeld { get; private set; } public float VelocityY { get; set; } public bool IsGrounded { get; private set; } public bool WasGrounded { get; private set; } public string CurrentStateName => currentState != null ? currentState.GetType().Name : "None"; public static PlayerStateMachine Local { get; private set; } public Quaternion CameraRotation { get { if (Runner != null && Runner.IsRunning && Object != null) return NetworkedCameraRotation; return Cam != null ? Cam.PlanarRotation : transform.rotation; } } private PlayerBaseState currentState; private bool hasControl = true; private bool hasSpeedParam; private bool hasVelocityXParam; private bool hasVelocityZParam; private List interactablesNearby = new List(); private int currentInteractableIndex = 0; private float localAnimatorSpeed; protected virtual void Awake() { Controller = GetComponent(); Input = GetComponent(); Anim = GetComponentInChildren(); Scanner = GetComponent(); if (Anim != null) { foreach (AnimatorControllerParameter param in Anim.parameters) { if (param.name == speedParamName) hasSpeedParam = true; if (param.name == velocityXParamName) hasVelocityXParam = true; if (param.name == velocityZParamName) hasVelocityZParam = true; } } speedHash = Animator.StringToHash(speedParamName); velocityXHash = Animator.StringToHash(velocityXParamName); velocityZHash = Animator.StringToHash(velocityZParamName); } private void Start() { if (Runner == null || !Runner.IsRunning) InitializePlayer(); } public override void Spawned() { InitializePlayer(); if (Object != null && !Object.HasInputAuthority && Runner.IsClient) { if (Controller != null) Controller.enabled = false; } } void OnHealthChangedRender() => OnHealthChanged?.Invoke(Health); void OnStaminaChangedRender() => OnStaminaChanged?.Invoke(Stamina); void OnNoiseLevelChangedRender() => OnNoiseLevelChanged?.Invoke(NoiseLevel); private void InitializePlayer() { if (currentState == null) SwitchState(new PlayerIdleState(this)); bool isOffline = Runner == null || !Runner.IsRunning; if (isOffline || (Object != null && Object.HasInputAuthority)) { Local = this; CameraController cameraController = GameObject.FindAnyObjectByType(); if (cameraController != null) { Cam = cameraController; Cam.followTarget = transform; Cam.inputReader = Input; } Input.OnNextInteractEvent += OnNextInteract; Input.OnPreviousInteractEvent += OnPreviousInteract; if (Controller != null) Controller.enabled = true; } } public void Rotate(Vector3 moveDirection, float deltaTime) { if (moveDirection == Vector3.zero) return; Quaternion targetRot = Quaternion.LookRotation(moveDirection); transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, RotationSpeed * deltaTime); } public void Move(Vector3 velocity, float animatorSpeed, float deltaTime) { bool canMove = (Runner == null || !Runner.IsRunning) || Object.HasInputAuthority || Runner.IsServer; if (!canMove) return; if (Controller != null && Controller.enabled) { Controller.Move(velocity * deltaTime); if (Object != null && Runner != null && Runner.IsRunning) NetworkedPosition = transform.position; } localAnimatorSpeed = animatorSpeed; if (Object != null && Object.HasStateAuthority) { NetworkedSpeed = animatorSpeed; NetworkedMoveInput = MoveInput; } UpdateAnimator(deltaTime); } private void UpdateAnimator(float deltaTime) { if (Anim == null) return; float speedValue = (Runner == null || !Runner.IsRunning || Object.HasInputAuthority) ? localAnimatorSpeed : NetworkedSpeed; Vector2 inputVector = (Runner == null || !Runner.IsRunning || Object.HasInputAuthority) ? MoveInput : NetworkedMoveInput; if (hasSpeedParam) Anim.SetFloat(speedHash, speedValue, AnimationDamping, deltaTime); if (hasVelocityXParam) Anim.SetFloat(velocityXHash, inputVector.x * speedValue, AnimationDamping, deltaTime); if (hasVelocityZParam) Anim.SetFloat(velocityZHash, inputVector.y * speedValue, AnimationDamping, deltaTime); } public override void FixedUpdateNetwork() { bool isRunning = Runner != null && Runner.IsRunning; if (Object == null && isRunning) return; if (isRunning && NetworkedPosition != Vector3.zero && !Object.HasInputAuthority) { Controller.enabled = false; transform.position = NetworkedPosition; Controller.enabled = true; } if (GetInput(out PlayerInputData data)) { MoveInput = data.Direction; IsSprintHeld = data.sprint; if (isRunning) NetworkedCameraRotation = data.rot; } else if (!isRunning) { MoveInput = new Vector2(UnityEngine.Input.GetAxisRaw("Horizontal"), UnityEngine.Input.GetAxisRaw("Vertical")); IsSprintHeld = UnityEngine.Input.GetKey(KeyCode.LeftShift); } else { MoveInput = Vector2.zero; IsSprintHeld = false; } if (!isRunning || Object.HasInputAuthority || Runner.IsServer) { if (hasControl) { WasGrounded = IsGrounded; CheckGround(); UpdateInteractablesList(); currentState?.Tick(isRunning ? Runner.DeltaTime : Time.fixedDeltaTime); } } else { UpdateAnimator(Runner.DeltaTime); } } private void Update() { if (Runner == null || !Runner.IsRunning) FixedUpdateNetwork(); } private void CheckGround() => IsGrounded = Physics.CheckSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius, GroundMask); private void UpdateInteractablesList() { interactablesNearby.Clear(); IInteractable target = Scanner.ScanForInteractable(InteractionRange, InteractionMask); if (target != null) interactablesNearby.Add(target); OnInteractableTargetChanged?.Invoke(target); currentInteractableIndex = 0; } private void OnNextInteract() { if (interactablesNearby.Count <= 1) return; currentInteractableIndex = (currentInteractableIndex + 1) % interactablesNearby.Count; OnInteractableTargetChanged?.Invoke(GetInteractable()); } private void OnPreviousInteract() { if (interactablesNearby.Count <= 1) return; currentInteractableIndex = (currentInteractableIndex - 1 + interactablesNearby.Count) % interactablesNearby.Count; OnInteractableTargetChanged?.Invoke(GetInteractable()); } public IInteractable GetInteractable() => interactablesNearby.Count == 0 ? null : interactablesNearby[currentInteractableIndex]; public void SetGroundCheck(float radius, Vector3 offset) { GroundCheckRadius = radius; GroundCheckOffset = offset; } public void SwitchState(PlayerBaseState newState) { currentState?.Exit(); currentState = newState; currentState?.Enter(); } public void SetControl(bool control) { hasControl = control; if (Controller != null) Controller.enabled = control; if (!control && Anim != null) Anim.SetFloat(speedHash, 0f); } private void OnDrawGizmosSelected() { Gizmos.color = new Color(0, 1, 0, 0.5f); Gizmos.DrawSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius); } } }