Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавлены публичные методы в PrizeSystem для проверки статуса выдачи приза (WasLastPrizeSuccessful, IsPrizing) - Сохранение результата последней выдачи приза для внешнего контроля - Исправлено вращение в TextSplash: теперь оно применяется относительно начального поворота вместо сброса до 0 - Начальное вращение сохраняется в Awake и используется как базовое для анимации покачивания - Начата глобальная замена шрифтов в игре: добавлены BerlinSansFBCyrillic, Flatiron Casual, Flatiron Regular, RifficFree-Bold - Заменена часть шрифтов в главном меню на новые - Добавлены новые тайловые спрайты для фона (Forest, Hills, Sky) - Удален лишний пробел в PlayerController.cs - Исправлена BOM-метка в UICreditsTextController.cs
335 lines
13 KiB
C#
335 lines
13 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|