From 55e4857af412da1cb3e05d272b864157e44f1db1 Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Thu, 26 Feb 2026 15:46:38 +0700 Subject: [PATCH] =?UTF-8?q?[Add]=20DiceRoller=20=E2=80=94=20beautiful=20ph?= =?UTF-8?q?ysics-based=20dice=20throw=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a coroutine-driven roller that tosses the dice with random impulse and torque, waits for physics to settle, then smoothly snaps to the nearest aligned face via Slerp. Exposes OnRollFinished event with the rolled value. Co-Authored-By: Claude Opus 4.6 --- Assets/Scripts/Dice/DiceRoller.cs | 157 ++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Assets/Scripts/Dice/DiceRoller.cs diff --git a/Assets/Scripts/Dice/DiceRoller.cs b/Assets/Scripts/Dice/DiceRoller.cs new file mode 100644 index 0000000..9e38bff --- /dev/null +++ b/Assets/Scripts/Dice/DiceRoller.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections; +using UnityEngine; +using Random = UnityEngine.Random; + +/// +/// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится, +/// плавно доворачивает до ровного положения и сообщает результат. +/// +public sealed class DiceRoller : MonoBehaviour +{ + // ─────────────────────────── Inspector ─────────────────────────── + + [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); + + // ─────────────────────────── Events ────────────────────────────── + + /// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение. + public event Action OnRollFinished; + + // ─────────────────────────── State ─────────────────────────────── + + /// Идёт ли сейчас бросок. + public bool IsRolling { get; private set; } + + private Coroutine rollRoutine; + + // ─────────────────────────── Unity ─────────────────────────────── + + private void Reset() + { + dice = GetComponent(); + rb = GetComponent(); + } + + // ─────────────────────────── Public API ────────────────────────── + + /// Бросить кубик. Повторный вызов во время броска игнорируется. + public void Roll() + { + if (IsRolling) return; + + if (rollRoutine != null) + StopCoroutine(rollRoutine); + + rollRoutine = StartCoroutine(RollSequence()); + } + + // ─────────────────────────── Core ──────────────────────────────── + + 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($"🎲 Выпало: {topValue}"); + } +}