using UnityEngine; using UnityEngine.InputSystem; using RR.Common.Interfaces; using RR.Game2D.Core.Managers; using RR.Game2D.Behaviour.Camera; using RR.Game2D.Behaviour.ScriptableObjects; namespace RR.Game2D.Behaviour { [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody2D), typeof(Collider2D), typeof(Animator))] public class PlayerController : MonoBehaviour, IDamageable { [Header("Управление")] [SerializeField] private InputActionReference moveAction; [SerializeField] private InputActionReference jumpAction; [Header("Горизонтальное движение")] [SerializeField] private float moveSpeed = 8f; [SerializeField] private float accel = 40f; [SerializeField] private float decel = 20f; [Header("Вертикальное движение")] [SerializeField] public float jumpVelocity = 50f; [SerializeField] private float jumpCut = 2f; public bool IsGrounded => Grounded(); [Header("Проверка призмеления")] [SerializeField] private Transform groundCheck; [SerializeField] private float groundCheckRadius = 0.05f; [SerializeField] private LayerMask groundMask; [Header("Антизастревание")] [SerializeField] private float stuckThreshold = 0.3f; // Время до срабатывания (сек) [SerializeField] private float stuckSpeedThreshold = 0.5f; // Макс скорость для "застревания" [SerializeField] private float unstuckForce = 15f; // Сила выталкивания вверх [SerializeField] private float unstuckCooldown = 0.5f; // Задержка между выталкиваниями [Header("Аниматор [Опционально]")] [SerializeField] private Animator animator; // оставить в инспекторе на всякий случай [Header("Здоровье")] [SerializeField] private int maxHealth = 3; private Health _health; public float CurrentHealth => _health?.CurrentHealth ?? 0; public float MaxHealth => _health?.MaxHealth ?? 0; [Header("FX")] [SerializeField] public SFXAsset stepSounds; [SerializeField] public SFXAsset hurtSounds; [SerializeField] public SFXAsset jumpSounds; private Rigidbody2D rb; // private Collider2D col; private float _inputX; private bool _jumpPressed, _jumpReleased, _jumpHold; private float _fallSpeedYDampingChangeThreshold; // Антизастревание private float _stuckTime; private float _unstuckCooldownTimer; // Аниматор private static readonly int Jump = Animator.StringToHash("Jump"); private static readonly int Speed = Animator.StringToHash("Speed"); private static readonly int VSpeed = Animator.StringToHash("VSpeed"); private static readonly int GroundedAnimator = Animator.StringToHash("Grounded"); private static readonly int Death = Animator.StringToHash("Death"); private void OnEnable() { moveAction.action.Enable(); jumpAction.action.Enable(); } private void OnDisable() { moveAction.action.Disable(); jumpAction.action.Disable(); } private void Awake() { rb = GetComponent(); //col = GetComponent(); rb.constraints = RigidbodyConstraints2D.FreezeRotation; _fallSpeedYDampingChangeThreshold = CameraManager.instance._fallSpeedYDampingChangeThreshold; // если не задано в инспекторе — находим автоматически if (animator == null) animator = GetComponent(); if (animator == null) animator = GetComponentInChildren(true); if (animator == null) Debug.LogError($"[{name}] Animator не найден!"); _health = GetComponent(); if (_health == null) { _health = gameObject.AddComponent(); } _health.ResetHealth(); _health.SetOwner(this); _health.SetMaxHealth(maxHealth); } private void Update() { // --- Ввод --- Vector2 move = moveAction ? moveAction.action.ReadValue() : Vector2.zero; _inputX = Mathf.Abs(move.x) > 0.01f ? Mathf.Sign(move.x) : 0f; _jumpPressed |= jumpAction.action.WasPressedThisFrame(); _jumpReleased |= jumpAction.action.WasReleasedThisFrame(); _jumpHold |= jumpAction.action.IsPressed(); if (_inputX != 0f) transform.localEulerAngles = Mathf.Sign(_inputX) > 0 ? Vector3.zero : new Vector3(0, 180f, 0); if (rb.linearVelocity.y < _fallSpeedYDampingChangeThreshold && !CameraManager.instance.IsLerpingYDamping && !CameraManager.instance.LerpedFromPlayerFalling) { CameraManager.instance.LerpYDamping(true); } if (rb.linearVelocity.y >= 0 && !CameraManager.instance.IsLerpingYDamping && CameraManager.instance.LerpedFromPlayerFalling) { CameraManager.instance.LerpedFromPlayerFalling = false; CameraManager.instance.LerpYDamping(false); } // --- Анимации --- animator.SetFloat(Speed, Mathf.Abs(rb.linearVelocity.x)); animator.SetFloat(VSpeed, rb.linearVelocity.y); animator.SetBool(GroundedAnimator, IsGrounded); } private void FixedUpdate() { animator.SetBool(Jump, false); // Горизонтальное движение float target = _inputX * moveSpeed; float rate = (Mathf.Abs(target) > 0.01) ? accel : decel; float velocityX = Mathf.MoveTowards(rb.linearVelocity.x, target, rate * Time.fixedDeltaTime); Vector2 newVelocity = rb.linearVelocity; newVelocity.x = velocityX; // Вертикальное движение if ((_jumpPressed || _jumpHold) && IsGrounded) { animator.SetBool(Jump, true); newVelocity.y = jumpVelocity; } if (_jumpReleased && newVelocity.y > 0) newVelocity.y /= Mathf.Max(1f, jumpCut); rb.linearVelocity = newVelocity; _jumpPressed = _jumpReleased = _jumpHold = false; // Система антизастревания CheckAndHandleStuck(); } private void OnTriggerEnter2D(Collider2D collision) { if (collision.CompareTag("Finish")) { GameManager.Instance.NextLevel(); } if (collision.CompareTag("Enemy")) { TakeDamage(1f); // GameManager.Logger.Log(ToString(), "Took damage from enemy collision."); // GameManager.Logger.Log(ToString(), $"Collision with: {collision.name}"); } } public void TakeDamage(float amount) { _health.TakeDamage(amount); } public void Heal(float amount) { _health.Heal(amount); } public void Die() { gameObject.layer = LayerMask.NameToLayer("No Collision"); rb.constraints = RigidbodyConstraints2D.FreezeAll; animator.SetTrigger(Death); } /// /// Проверяет, застрял ли игрок, и выталкивает его если нужно /// private void CheckAndHandleStuck() { // Уменьшаем таймер кулдауна if (_unstuckCooldownTimer > 0) { _unstuckCooldownTimer -= Time.fixedDeltaTime; return; } // Проверяем условия застревания: // 1. Игрок пытается двигаться (есть ввод) // 2. Скорость по X близка к нулю (не движется) // 3. Касается чего-то (земли или стены) bool tryingToMove = Mathf.Abs(_inputX) > 0.1f; bool notMoving = Mathf.Abs(rb.linearVelocity.x) < stuckSpeedThreshold; bool hasCollision = IsGrounded || CheckWallContact(); if (tryingToMove && notMoving && hasCollision) { // Накапливаем время застревания _stuckTime += Time.fixedDeltaTime; // Если застряли достаточно долго - выталкиваем if (!(_stuckTime >= stuckThreshold)) return; UnstuckPlayer(); _stuckTime = 0f; _unstuckCooldownTimer = unstuckCooldown; } else { // Игрок двигается нормально - сбрасываем счетчик _stuckTime = 0f; } } /// /// Проверяет, касается ли игрок стены с боков /// private bool CheckWallContact() { // Проверяем коллизии с боков Vector2 boxSize = new Vector2(0.1f, 0.5f); Vector2 leftCheck = (Vector2)transform.position + Vector2.left * 0.3f; Vector2 rightCheck = (Vector2)transform.position + Vector2.right * 0.3f; bool leftWall = Physics2D.OverlapBox(leftCheck, boxSize, 0f, groundMask); bool rightWall = Physics2D.OverlapBox(rightCheck, boxSize, 0f, groundMask); return leftWall || rightWall; } /// /// Выталкивает игрока из застревания /// private void UnstuckPlayer() { // Пробуем найти свободную позицию выше текущей Vector3 freePosition = FindFreePosition(); if (freePosition != Vector3.zero) { // Телепортируем в свободную позицию transform.position = freePosition; } // Добавляем импульс вверх для гарантии Vector2 unstuckVelocity = rb.linearVelocity; unstuckVelocity.y = unstuckForce; rb.linearVelocity = unstuckVelocity; Debug.Log($"[PlayerController] Unstuck player at {transform.position}"); } /// /// Ищет ближайшую свободную позицию над игроком /// private Vector3 FindFreePosition() { // Проверяем позиции в��ше текущей с шагом 0.1 единиц float checkDistance = 0.05f; int maxAttempts = 10; for (int i = 1; i <= maxAttempts; i++) { Vector3 testPosition = transform.position + Vector3.up * (checkDistance * i); // Проверяем, свободна ли эта позиция Collider2D hit = Physics2D.OverlapCircle(testPosition, groundCheckRadius, groundMask); if (hit == null) { return testPosition; } } // Если не нашли свободную позицию, возвращаем небольшое смещение вверх return transform.position + Vector3.up * 0.1f; } /// /// Метод для проверки соприкосновения с землей /// /// bool isGrounded - на земле ли игрок private bool Grounded() { Collider2D hit = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundMask); bool isGrounded = hit != null && hit.CompareTag("Ground"); //&& rb.linearVelocity.y < 0.01f; // animator.SetBool(Jump, !isGrounded); return isGrounded; } } }