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; } 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; } [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; // Sự kiện để UI lắng nghe 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; } 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(); 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; } } // Callbacks từ attribute OnChangedRender 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; 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; 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) { 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; Vector2 inputVector; if (Runner == null || !Runner.IsRunning || Object.HasInputAuthority) { speedValue = localAnimatorSpeed; inputVector = MoveInput; } else { speedValue = NetworkedSpeed; inputVector = 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) { 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; 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; } 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() { 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); } else { OnInteractableTargetChanged?.Invoke(null); } 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--; if (currentInteractableIndex < 0) currentInteractableIndex = interactablesNearby.Count - 1; OnInteractableTargetChanged?.Invoke(GetInteractable()); } 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); } } }