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(); sphereRadius = GetComponent().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(); } void FixedUpdate() { CheckGrounded(); HandleMovement(); } // Свойство для проверки состояния "на земле" из других скриптов 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(); 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(); 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() { // Не обрабатываем физику, если игра окончена 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); } } }