320 lines
12 KiB
C#
320 lines
12 KiB
C#
using UnityEngine;
|
||
|
||
public class BallController : MonoBehaviour
|
||
{
|
||
[Header("Ссылки")]
|
||
private Transform currentPlatform = null;
|
||
[SerializeField] private GameObject gameOverPanel;
|
||
|
||
[Header("Настройки Движения")]
|
||
[SerializeField] private float maxSpeed = 15f; // Максимальная скорость на земле
|
||
[SerializeField] private float accelerationRate = 200f; // Как быстро шарик разгоняется
|
||
[SerializeField] private float airControlSpeed = 5f; // Скорость управления в прыжке
|
||
[SerializeField] private float jumpForce = 7f; // Сила прыжка
|
||
[SerializeField] private float trampolineBounceForce = 1.5f; // Множитель для отскока от батута
|
||
|
||
[Header("Настройки Контроля")]
|
||
[Tooltip("Сила торможения/трения. Применяется, когда нет ввода.")]
|
||
[SerializeField] private float decelerationRate = 5f; // Сила, замедляющая шарик (трение)
|
||
|
||
[SerializeField] private float groundCheckDistance = 0.1f; // Дистанция для проверки земли
|
||
|
||
[SerializeField] private float fallTimeLimit = 10f;
|
||
|
||
private Rigidbody rb;
|
||
private bool isGrounded;
|
||
private float sphereRadius;
|
||
|
||
// Переменные для считывания ввода
|
||
private float moveX;
|
||
private float moveZ;
|
||
|
||
private float timeNotInAir = 0f;
|
||
private bool isGameOver = false;
|
||
void Start()
|
||
{
|
||
rb = GetComponent<Rigidbody>();
|
||
sphereRadius = GetComponent<SphereCollider>().radius;// Убедимся, что при старте игры панель выключена
|
||
if (gameOverPanel != null)
|
||
{
|
||
gameOverPanel.SetActive(false);
|
||
}
|
||
|
||
// Сброс TimeScale на случай, если предыдущая игра его остановила
|
||
Time.timeScale = 1f;
|
||
isGameOver = false;
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
// 1. Считываем ввод в Update
|
||
moveX = Input.GetAxisRaw("Horizontal");
|
||
moveZ = Input.GetAxisRaw("Vertical");
|
||
|
||
if (isGameOver) return;
|
||
|
||
HandleJumpInput();
|
||
}
|
||
|
||
|
||
|
||
// Свойство для проверки состояния "на земле" из других скриптов
|
||
public bool IsGrounded
|
||
{
|
||
get { return isGrounded; }
|
||
}
|
||
|
||
private void CheckGrounded()
|
||
{
|
||
isGrounded = false;
|
||
Vector3 origin = transform.position;
|
||
if (Physics.Raycast(origin, -Vector3.up, out RaycastHit hit, sphereRadius + groundCheckDistance))
|
||
{
|
||
if (hit.normal.y > 0.7f)
|
||
{
|
||
isGrounded = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Логика Управления Движением ---
|
||
|
||
private void HandleMovement()
|
||
{
|
||
if (isGrounded)
|
||
{
|
||
// Определяем, есть ли ввод от игрока
|
||
bool hasInput = Mathf.Abs(moveX) > 0.01f || Mathf.Abs(moveZ) > 0.01f;
|
||
|
||
if (hasInput)
|
||
{
|
||
ApplyAcceleration();
|
||
}
|
||
else
|
||
{
|
||
ApplyDeceleration();
|
||
}
|
||
|
||
// Ограничение максимальной скорости (применяется всегда, если шарик на земле)
|
||
ClampHorizontalSpeed();
|
||
}
|
||
else
|
||
{
|
||
// Управление в воздухе
|
||
Vector3 movement = new Vector3(moveX, 0.0f, moveZ).normalized;
|
||
rb.AddForce(movement * airControlSpeed, ForceMode.Acceleration);
|
||
}
|
||
}
|
||
|
||
private void ApplyAcceleration()
|
||
{
|
||
Vector3 currentVel = rb.linearVelocity;
|
||
|
||
// 1. Ускорение по X (A/D)
|
||
if (Mathf.Abs(moveX) > 0.01f)
|
||
{
|
||
if (Mathf.Abs(currentVel.x) < maxSpeed)
|
||
{
|
||
rb.AddForce(new Vector3(moveX, 0, 0) * accelerationRate, ForceMode.Acceleration);
|
||
}
|
||
}
|
||
|
||
// 2. Ускорение по Z (W/S)
|
||
if (Mathf.Abs(moveZ) > 0.01f)
|
||
{
|
||
if (Mathf.Abs(currentVel.z) < maxSpeed)
|
||
{
|
||
rb.AddForce(new Vector3(0, 0, moveZ) * accelerationRate, ForceMode.Acceleration);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ApplyDeceleration()
|
||
{
|
||
Vector3 horizontalVel = new Vector3(rb.linearVelocity.x, 0, rb.linearVelocity.z);
|
||
|
||
// Применяем силу, противоположную скорости (имитация трения)
|
||
if (horizontalVel.sqrMagnitude > 0.01f)
|
||
{
|
||
// Направление силы, противоположное скорости
|
||
Vector3 frictionForce = -horizontalVel.normalized * decelerationRate;
|
||
rb.AddForce(frictionForce, ForceMode.Acceleration);
|
||
}
|
||
else
|
||
{
|
||
// Если скорость очень низка, останавливаем горизонтальное движение, чтобы избежать "дрейфа"
|
||
rb.linearVelocity = new Vector3(0, rb.linearVelocity.y, 0);
|
||
}
|
||
}
|
||
|
||
private void ClampHorizontalSpeed()
|
||
{
|
||
Vector3 horizontalVel = new Vector3(rb.linearVelocity.x, 0, rb.linearVelocity.z);
|
||
|
||
if (horizontalVel.sqrMagnitude > maxSpeed * maxSpeed)
|
||
{
|
||
Vector3 clampedVel = horizontalVel.normalized * maxSpeed;
|
||
// Устанавливаем горизонтальную скорость, сохраняя вертикальную
|
||
rb.linearVelocity = new Vector3(clampedVel.x, rb.linearVelocity.y, clampedVel.z);
|
||
}
|
||
}
|
||
|
||
private void HandleJumpInput()
|
||
{
|
||
if (Input.GetButtonDown("Jump") && isGrounded)
|
||
{
|
||
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
|
||
}
|
||
}
|
||
|
||
// --- Логика Взаимодействия и Тегирования ---
|
||
|
||
private void OnCollisionEnter(Collision collision)
|
||
{
|
||
if (collision.gameObject.CompareTag("Trampoline"))
|
||
{
|
||
// Случай 3: БАТУТ -> Моментальный отскок
|
||
|
||
// Если мы уже прилипли к чему-то, отлипаем
|
||
if (currentPlatform != null)
|
||
{
|
||
transform.SetParent(null);
|
||
currentPlatform = null;
|
||
}
|
||
|
||
// *** ГЛАВНОЕ ИЗМЕНЕНИЕ: Находим нормаль к поверхности ***
|
||
|
||
// Получаем нормаль к поверхности в точке столкновения.
|
||
// Это и есть направление, перпендикулярное поверхности.
|
||
Vector3 surfaceNormal = Vector3.up;
|
||
if (collision.contacts.Length > 0)
|
||
{
|
||
// Используем нормаль первого контакта для определения направления отскока
|
||
surfaceNormal = collision.contacts[0].normal;
|
||
}
|
||
|
||
// Применяем сильный импульс в направлении нормали
|
||
// Сбрасываем текущую вертикальную скорость шарика, чтобы отскок был одинаковым
|
||
// Для более физически корректного отскока, мы можем очистить всю скорость
|
||
rb.linearVelocity = Vector3.zero;
|
||
|
||
// Применяем силу, направленную строго по нормали поверхности.
|
||
rb.AddForce(surfaceNormal * jumpForce * trampolineBounceForce, ForceMode.Impulse);
|
||
|
||
return; // Завершаем проверку, это батут, прилипать не нужно
|
||
}
|
||
// 2. Проверяем на ДВИЖУЩУЮСЯ ПЛАТФОРМУ
|
||
MovingPlatform platformScript = collision.gameObject.GetComponent<MovingPlatform>();
|
||
|
||
if (platformScript != null)
|
||
{
|
||
// Случай 1: ДВИЖУЩАЯСЯ ПЛАТФОРМА -> Прилипаем
|
||
if (isGrounded)
|
||
{
|
||
transform.SetParent(collision.transform);
|
||
currentPlatform = collision.transform;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Случай 2: СТАТИЧЕСКАЯ ЗЕМЛЯ (обычная неподвижная платформа)
|
||
|
||
// Гарантируем, что мы не прилипли, если это не MovingPlatform
|
||
if (currentPlatform != null)
|
||
{
|
||
transform.SetParent(null);
|
||
currentPlatform = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnCollisionStay(Collision collision)
|
||
{
|
||
CheckIfGrounded(collision);
|
||
}
|
||
|
||
private void OnCollisionExit(Collision collision)
|
||
{
|
||
// Логика отлипания: Сбрасываем родителя только если покидаем MovingPlatform
|
||
MovingPlatform platformScript = collision.gameObject.GetComponent<MovingPlatform>();
|
||
if (platformScript != null)
|
||
{
|
||
transform.SetParent(null);
|
||
currentPlatform = null;
|
||
}
|
||
// Для батута и статики отлипание уже произошло в OnCollisionEnter
|
||
}
|
||
|
||
// Вспомогательный метод для проверки земли
|
||
private void CheckIfGrounded(Collision collision)
|
||
{
|
||
|
||
if (collision.contacts.Length > 0)
|
||
{
|
||
Vector3 normal = collision.contacts[0].normal;
|
||
if (normal.y > 0.7f)
|
||
{
|
||
isGrounded = true;
|
||
}
|
||
}
|
||
HandleTrampolineBounce(collision);
|
||
|
||
}
|
||
|
||
private void HandleTrampolineBounce(Collision collision)
|
||
{
|
||
Vector3 surfaceNormal = Vector3.up;
|
||
if (collision.contacts.Length > 0)
|
||
{
|
||
surfaceNormal = collision.contacts[0].normal;
|
||
}
|
||
// Обнуляем скорость перед отскоком для стабильности
|
||
rb.linearVelocity = Vector3.zero;
|
||
rb.AddForce(surfaceNormal * jumpForce * trampolineBounceForce, ForceMode.Impulse);
|
||
}
|
||
|
||
void FixedUpdate()
|
||
{
|
||
CheckGrounded();
|
||
// Не обрабатываем физику, если игра окончена
|
||
if (isGameOver) return;
|
||
|
||
// Мы проверяем 'isGrounded' из *предыдущего* физического кадра.
|
||
if (isGrounded)
|
||
{
|
||
// Если были на земле, сбрасываем таймер
|
||
timeNotInAir = 0f;
|
||
}
|
||
else
|
||
{
|
||
// Если в воздухе, увеличиваем таймер
|
||
timeNotInAir += Time.fixedDeltaTime;
|
||
|
||
if (timeNotInAir >= fallTimeLimit)
|
||
{
|
||
GameOver();
|
||
}
|
||
}
|
||
|
||
// Теперь сбрасываем isGrounded.
|
||
// OnCollisionStay в этом кадре установит его обратно в 'true',
|
||
// если шарик все еще касается земли.
|
||
isGrounded = false;
|
||
|
||
HandleMovement();
|
||
}
|
||
|
||
private void GameOver()
|
||
{
|
||
if (isGameOver) return;
|
||
|
||
isGameOver = true;
|
||
|
||
Time.timeScale = 0f;
|
||
|
||
if (gameOverPanel != null)
|
||
{
|
||
gameOverPanel.SetActive(true);
|
||
}
|
||
}
|
||
} |