using System; using System.Collections; using UnityEngine; using Random = UnityEngine.Random; /// /// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится, /// плавно доворачивает до ровного положения и сообщает результат. /// public sealed class DiceRoller : MonoBehaviour { [Header("References")] [SerializeField] private Dice dice; [SerializeField] private Rigidbody rb; [Header("Throw Settings")] [Tooltip("Сила подброса вверх")] [SerializeField] private float throwUpForce = 6f; [Tooltip("Горизонтальный разброс")] [SerializeField] private float throwScatter = 1.5f; [Tooltip("Минимальный крутящий момент")] [SerializeField] private float torqueMin = 15f; [Tooltip("Максимальный крутящий момент")] [SerializeField] private float torqueMax = 30f; [Header("Settle Detection")] [Tooltip("Порог скорости, ниже которого кубик считается остановившимся")] [SerializeField] private float settleSpeed = 0.05f; [Tooltip("Сколько секунд кубик должен быть неподвижен, чтобы засчитать остановку")] [SerializeField] private float settleDelay = 0.3f; [Header("Snap Alignment")] [Tooltip("Длительность плавного доворота к ровному положению")] [SerializeField] private float snapDuration = 0.35f; [SerializeField] private AnimationCurve snapCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); /// /// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение. /// public event Action OnRollFinished; /// /// Идёт ли сейчас бросок. /// public bool IsRolling { get; private set; } private Coroutine rollRoutine; private void Reset() { dice = GetComponent(); rb = GetComponent(); } /// /// Бросить кубик. Повторный вызов во время броска игнорируется. /// public void Roll() { if (IsRolling) return; if (rollRoutine != null) StopCoroutine(rollRoutine); rollRoutine = StartCoroutine(RollSequence()); } private IEnumerator RollSequence() { IsRolling = true; // ── 1. Подготовка ──────────────────────────────────────────── rb.isKinematic = false; rb.useGravity = true; rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; // ── 2. Импульс: подбросить вверх с лёгким разбросом ───────── Vector3 force = new Vector3( Random.Range(-throwScatter, throwScatter), throwUpForce, Random.Range(-throwScatter, throwScatter) ); rb.AddForce(force, ForceMode.Impulse); // ── 3. Случайный крутящий момент — красивое вращение ──────── Vector3 torque = Random.onUnitSphere * Random.Range(torqueMin, torqueMax); rb.AddTorque(torque, ForceMode.Impulse); // Даём кубику взлететь yield return new WaitForSeconds(0.2f); // ── 4. Ждём пока кубик успокоится ─────────────────────────── float stillTimer = 0f; float sqrThreshold = settleSpeed * settleSpeed; while (stillTimer < settleDelay) { yield return new WaitForFixedUpdate(); bool isSlow = rb.linearVelocity.sqrMagnitude < sqrThreshold && rb.angularVelocity.sqrMagnitude < sqrThreshold; stillTimer = isSlow ? stillTimer + Time.fixedDeltaTime : 0f; } // ── 5. Замораживаем физику и читаем верхнюю грань ─────────── rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; rb.isKinematic = true; if (!dice.TryGetTopValue(out int topValue)) { Debug.LogWarning("DiceRoller: не удалось определить верхнюю грань."); IsRolling = false; yield break; } // ── 6. Плавный доворот до ровного положения ───────────────── Quaternion startRot = transform.rotation; // Вычисляем целевой поворот через Dice.AlignToTopByLocalAngles dice.AlignToTopByLocalAngles(); Quaternion targetRot = transform.rotation; // Откатываемся обратно — будем интерполировать transform.rotation = startRot; float elapsed = 0f; while (elapsed < snapDuration) { elapsed += Time.deltaTime; float t = snapCurve.Evaluate(Mathf.Clamp01(elapsed / snapDuration)); transform.rotation = Quaternion.Slerp(startRot, targetRot, t); yield return null; } transform.rotation = targetRot; // ── 7. Готово ─────────────────────────────────────────────── IsRolling = false; OnRollFinished?.Invoke(topValue); Debug.Log($"{gameObject.name} | Выпало: {topValue}"); } }