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; } 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; } private List interactablesNearby = new List(); private int currentInteractableIndex = 0; public string CurrentStateName => currentState != null ? currentState.GetType().Name : "None"; public static PlayerStateMachine Local { get; private set; } private PlayerBaseState currentState; private bool hasControl = true; private bool hasSpeedParam; private bool hasVelocityXParam; private bool hasVelocityZParam; protected virtual void Awake() { Controller = GetComponent(); Input = GetComponent(); Anim = GetComponentInChildren(); Scanner = GetComponent(); // Kiểm tra tham số có tồn tại trong Animator không để tránh lỗi log gây Disconnect 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); } public override void Spawned() { SwitchState(new PlayerIdleState(this)); if (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; } else { // Vô hiệu hóa Controller của người chơi khác trên máy khách để tránh xung đột vật lý if (Runner.IsClient && Controller != null) Controller.enabled = false; } } private float localAnimatorSpeed; 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) { // CHỈ thực hiện di chuyển nếu có quyền điều khiển hoặc là Server if (!Object.HasInputAuthority && !Runner.IsServer) return; if (Controller != null && Controller.enabled) { Controller.Move(velocity * deltaTime); // Cập nhật vị trí mạng ngay sau khi di chuyển để tick sau quay lại đây NetworkedPosition = transform.position; } localAnimatorSpeed = animatorSpeed; if (Object.HasStateAuthority) { NetworkedSpeed = animatorSpeed; NetworkedMoveInput = MoveInput; } UpdateAnimator(deltaTime); } private void UpdateAnimator(float deltaTime) { if (Anim == null) return; float speedValue; Vector2 inputVector; if (Object.HasInputAuthority) { speedValue = localAnimatorSpeed; inputVector = MoveInput; } else { speedValue = NetworkedSpeed; inputVector = NetworkedMoveInput; } // Chỉ Set nếu tham số thực sự tồn tại (Tránh lỗi Hash does not exist) 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() { if (Object == null) return; // ĐỒNG BỘ VỊ TRÍ: Ép nhân vật về vị trí mạng trước khi tính toán tick mới // Điều này cực kỳ quan trọng để CharacterController không bị nhân đôi vận tốc khi Resimulation if (NetworkedPosition != Vector3.zero) { if (Controller != null) { // Tạm thời tắt Controller để dịch chuyển Transform chính xác Controller.enabled = false; transform.position = NetworkedPosition; Controller.enabled = true; } } if (GetInput(out PlayerInputData data)) { MoveInput = data.Direction; IsSprintHeld = data.sprint; NetworkedCameraRotation = data.rot; } else { MoveInput = Vector2.zero; IsSprintHeld = false; } if (!Object.HasInputAuthority && !Runner.IsServer) { UpdateAnimator(Runner.DeltaTime); return; } if (!hasControl) return; WasGrounded = IsGrounded; CheckGround(); UpdateInteractablesList(); currentState?.Tick(Runner.DeltaTime); } 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); currentInteractableIndex = 0; } private void OnNextInteract() { if (interactablesNearby.Count <= 1) return; currentInteractableIndex = (currentInteractableIndex + 1) % interactablesNearby.Count; } private void OnPreviousInteract() { if (interactablesNearby.Count <= 1) return; currentInteractableIndex--; if (currentInteractableIndex < 0) currentInteractableIndex = interactablesNearby.Count - 1; } public IInteractable GetInteractable() { if (interactablesNearby.Count == 0) return null; return 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); } } }