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; } // Thuộc tính hỗ trợ lấy rotation của Camera an toàn cho cả Online và Offline public Quaternion CameraRotation { get { if (Runner != null && Runner.IsRunning && Object != null) return NetworkedCameraRotation; if (Cam != null) return Cam.PlanarRotation; return transform.rotation; } } [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); } private void Start() { // Nếu chạy Offline (kéo prefab vào scene), Spawned() sẽ không được gọi. // Chúng ta khởi tạo tại đây để đảm bảo nhân vật hoạt động. if (Runner == null || !Runner.IsRunning) { InitializePlayer(); } } public override void Spawned() { // Fusion gọi Spawned khi object được nạp vào mạng. InitializePlayer(); // Nếu không có quyền điều khiển và đang ở Client, tắt Controller để tránh xung đột if (Object != null && !Object.HasInputAuthority && Runner.IsClient) { if (Controller != null) Controller.enabled = false; } } private void InitializePlayer() { if (currentState == null) { SwitchState(new PlayerIdleState(this)); } bool isOffline = Runner == null || !Runner.IsRunning; bool hasAuthority = Object != null && Object.HasInputAuthority; if (isOffline || hasAuthority) { Local = this; CameraController cameraController = GameObject.FindAnyObjectByType(); if (cameraController != null) { Cam = cameraController; Cam.followTarget = transform; Cam.inputReader = Input; } Input.OnNextInteractEvent += OnNextInteract; Input.OnPreviousInteractEvent += OnPreviousInteract; // Đảm bảo Controller được bật if (Controller != null) Controller.enabled = true; } } 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) { // Cho phép di chuyển nếu: // 1. Không có mạng (Offline test) // 2. Có quyền điều khiển (Input Authority) // 3. Là Server (State Authority) bool canMove = (Runner == null || !Runner.IsRunning) || Object.HasInputAuthority || Runner.IsServer; if (!canMove) return; if (Controller != null && Controller.enabled) { Controller.Move(velocity * deltaTime); // Cập nhật vị trí mạng ngay sau khi di chuyển 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; Vector2 inputVector; if (Runner == null || !Runner.IsRunning || 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() { bool isRunning = Runner != null && Runner.IsRunning; if (Object == null && isRunning) 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 if (isRunning && NetworkedPosition != Vector3.zero) { if (Controller != null && !Object.HasInputAuthority) { Controller.enabled = false; transform.position = NetworkedPosition; Controller.enabled = true; } } if (GetInput(out PlayerInputData data)) { MoveInput = data.Direction; IsSprintHeld = data.sprint; // Chỉ gán biến Networked nếu đang chạy mạng if (isRunning) NetworkedCameraRotation = data.rot; } else if (!isRunning) { // FALLBACK INPUT: Nếu không có Fusion, lấy input trực tiếp từ Unity để Test MoveInput = new Vector2(UnityEngine.Input.GetAxisRaw("Horizontal"), UnityEngine.Input.GetAxisRaw("Vertical")); IsSprintHeld = UnityEngine.Input.GetKey(KeyCode.LeftShift); // Ở chế độ offline, chúng ta không gán vào NetworkedCameraRotation nữa } else { MoveInput = Vector2.zero; IsSprintHeld = false; } bool isSimulating = !isRunning || Object.HasInputAuthority || Runner.IsServer; if (!isSimulating) { UpdateAnimator(Runner.DeltaTime); return; } if (!hasControl) return; WasGrounded = IsGrounded; CheckGround(); UpdateInteractablesList(); float dt = isRunning ? Runner.DeltaTime : Time.fixedDeltaTime; currentState?.Tick(dt); } private void Update() { // Nếu không có NetworkRunner, Fusion sẽ không gọi FixedUpdateNetwork. // Chúng ta gọi thủ công để logic StateMachine vẫn chạy được khi Test Offline. 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); 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); } } }