Danro/Assets/Scripts/Behaviour/Controllers/PlayerController.cs
Danil Iarosh 6a4c23c154
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: Add prize status API, fix TextSplash rotation, and update game fonts
- Добавлены публичные методы в PrizeSystem для проверки статуса выдачи приза (WasLastPrizeSuccessful, IsPrizing)
- Сохранение результата последней выдачи приза для внешнего контроля
- Исправлено вращение в TextSplash: теперь оно применяется относительно начального поворота вместо сброса до 0
- Начальное вращение сохраняется в Awake и используется как базовое для анимации покачивания
- Начата глобальная замена шрифтов в игре: добавлены BerlinSansFBCyrillic, Flatiron Casual, Flatiron Regular, RifficFree-Bold
- Заменена часть шрифтов в главном меню на новые
- Добавлены новые тайловые спрайты для фона (Forest, Hills, Sky)
- Удален лишний пробел в PlayerController.cs
- Исправлена BOM-метка в UICreditsTextController.cs
2026-02-17 13:29:26 +03:00

335 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
[HideInInspector] public bool isDeadAtMoment;
private Rigidbody2D rb;
// Ввод
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<Rigidbody2D>();
//col = GetComponent<Collider2D>();
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
_fallSpeedYDampingChangeThreshold = CameraManager.instance._fallSpeedYDampingChangeThreshold;
// если не задано в инспекторе — находим автоматически
if (animator == null)
animator = GetComponent<Animator>();
if (animator == null)
animator = GetComponentInChildren<Animator>(true);
if (animator == null)
Debug.LogError($"[{name}] Animator не найден!");
_health = GetComponent<Health>();
if (_health == null)
{
_health = gameObject.AddComponent<Health>();
}
_health.ResetHealth();
_health.SetOwner(this);
_health.SetMaxHealth(maxHealth);
}
private void Update()
{
// --- Ввод ---
Vector2 move = moveAction ? moveAction.action.ReadValue<Vector2>() : 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()
{
if (isDeadAtMoment) return;
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 Enemy Collision");
// rb.constraints = RigidbodyConstraints2D.FreezeAll;
isDeadAtMoment = true;
animator.SetTrigger(Death);
}
/// <summary>
/// Проверяет, застрял ли игрок, и выталкивает его если нужно
/// </summary>
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;
}
}
/// <summary>
/// Проверяет, касается ли игрок стены с боков
/// </summary>
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;
}
/// <summary>
/// Выталкивает игрока из застревания
/// </summary>
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}");
}
/// <summary>
/// Ищет ближайшую свободную позицию над игроком
/// </summary>
private Vector3 FindFreePosition()
{
// Проверяем позиции в<><D0B2>ше текущей с шагом 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;
}
/// <summary>
/// Метод для проверки соприкосновения с землей
/// </summary>
/// <returns>bool isGrounded - на земле ли игрок</returns>
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;
}
}
}