From dc82e93322bfececa85060faf79515ba2a3cb46c Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Sat, 7 Mar 2026 14:42:31 +0700 Subject: [PATCH] [Add] Dice return script --- Assets/Prefab/d6.prefab | 30 ++++++- Assets/Scenes/Scene.unity | 38 ++++----- Assets/Scripts/Dice/DiceRoller.cs | 127 ++++++++++++++++++++++++++++-- 3 files changed, 169 insertions(+), 26 deletions(-) diff --git a/Assets/Prefab/d6.prefab b/Assets/Prefab/d6.prefab index 87191ff..5c71ca9 100644 --- a/Assets/Prefab/d6.prefab +++ b/Assets/Prefab/d6.prefab @@ -311,13 +311,15 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::DiceRoller dice: {fileID: 3052556563113756475} rb: {fileID: 1768444232422188122} + diceCollider: {fileID: 0} k__BackingField: {fileID: 11400000, guid: b4fd0877395ff654b83ec79e811d356c, type: 2} throwUpForce: 2 throwScatter: 1.5 torqueMin: 15 - torqueMax: 30 + torqueMax: 80 settleSpeed: 0.05 settleDelay: 0.3 + maxRollDuration: 3 snapDuration: 0.35 snapCurve: serializedVersion: 2 @@ -343,6 +345,32 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 + returnDuration: 0.4 + returnArcHeight: 0.6 + returnCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 --- !u!1 &6346438093409117343 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scenes/Scene.unity b/Assets/Scenes/Scene.unity index 5dcdba8..f3cb4ed 100644 --- a/Assets/Scenes/Scene.unity +++ b/Assets/Scenes/Scene.unity @@ -543,11 +543,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.x - value: -2 + value: -2.01 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.y - value: 0 + value: 0.5 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.z @@ -762,7 +762,7 @@ Transform: m_GameObject: {fileID: 678730622} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 3.39505, y: 8.67578, z: 0} + m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -1173,9 +1173,9 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1284869687} serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956} - m_LocalPosition: {x: 0, y: -1.35, z: 0} - m_LocalScale: {x: 0.82, y: 0.82, z: 0.82} + m_LocalRotation: {x: -0, y: 0.38268343, z: -0, w: 0.92387956} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 0.8200002, y: 0.82, z: 0.8200002} m_ConstrainProportionsScale: 1 m_Children: - {fileID: 819220931} @@ -1272,7 +1272,7 @@ Transform: m_GameObject: {fileID: 1375693286} serializedVersion: 2 m_LocalRotation: {x: 0.46193978, y: 0.33141357, z: -0.19134171, w: 0.8001032} - m_LocalPosition: {x: -5, y: 25, z: -6} + m_LocalPosition: {x: -10, y: 22, z: -8} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -1352,11 +1352,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.x - value: -1 + value: -1.01 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.y - value: 0 + value: 0.5 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.z @@ -1409,11 +1409,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.x - value: 1 + value: 0.99 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.y - value: 0 + value: 0.5 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.z @@ -1535,11 +1535,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.x - value: 0 + value: -0.01 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.y - value: 0 + value: 0.5 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.z @@ -1592,11 +1592,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.x - value: 1 + value: 0.99 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.y - value: 0 + value: 0.5 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.z @@ -1662,8 +1662,8 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1715952346} serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: -2.2, z: 0} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 5, y: 5, z: 5} m_ConstrainProportionsScale: 0 m_Children: [] @@ -1778,11 +1778,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.x - value: 2 + value: 1.99 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.y - value: 0 + value: 0.5 objectReference: {fileID: 0} - target: {fileID: 6590077607741954368, guid: b59e9f48ded300a44bdf1f3f3a43e1ae, type: 3} propertyPath: m_LocalPosition.z diff --git a/Assets/Scripts/Dice/DiceRoller.cs b/Assets/Scripts/Dice/DiceRoller.cs index 1563555..845cdb6 100644 --- a/Assets/Scripts/Dice/DiceRoller.cs +++ b/Assets/Scripts/Dice/DiceRoller.cs @@ -7,13 +7,16 @@ namespace YachtDice.Dice { /// /// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится, - /// плавно доворачивает до ровного положения и сообщает результат. + /// плавно доворачивает до ровного положения, возвращает на старт и сообщает результат. /// public class DiceRoller : MonoBehaviour { + private static DiceRoller _activeReturnRoller; + [Header("References")] [SerializeField] private Dice dice; [SerializeField] private Rigidbody rb; + [SerializeField] private Collider diceCollider; [Tooltip("Определение типа дайса")] [field: SerializeField] public DiceDefinition Definition { get; private set; } @@ -38,12 +41,25 @@ namespace YachtDice.Dice [Tooltip("Сколько секунд кубик должен быть неподвижен, чтобы засчитать остановку")] [SerializeField] private float settleDelay = 0.3f; + [Tooltip("Максимальная длительность броска до принудительного завершения")] + [SerializeField] private float maxRollDuration = 3f; + [Header("Snap Alignment")] [Tooltip("Длительность плавного доворота к ровному положению")] [SerializeField] private float snapDuration = 0.35f; [SerializeField] private AnimationCurve snapCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); + + [Header("Return To Start")] + [Tooltip("Длительность возврата в стартовую позицию")] + [SerializeField] private float returnDuration = 0.4f; + + [Tooltip("Высота дуги при возврате в стартовую позицию")] + [SerializeField] private float returnArcHeight = 0.6f; + + [SerializeField] + private AnimationCurve returnCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); /// /// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение. @@ -56,11 +72,36 @@ namespace YachtDice.Dice public bool IsRolling { get; private set; } private Coroutine _rollRoutine; + private Vector3 _startLocalPosition; + private bool _startPoseCaptured; + + private void Awake() + { + if (dice == null) + dice = GetComponent(); + + if (rb == null) + rb = GetComponent(); + + if (diceCollider == null) + diceCollider = GetComponent(); + + CaptureStartPose(); + } private void Reset() { dice = GetComponent(); rb = GetComponent(); + diceCollider = GetComponent(); + } + + private void CaptureStartPose() + { + if (_startPoseCaptured) return; + + _startLocalPosition = transform.localPosition; + _startPoseCaptured = true; } @@ -71,6 +112,12 @@ namespace YachtDice.Dice { if (IsRolling) return; + if (dice == null || rb == null) + { + Debug.LogError("DiceRoller: отсутствуют обязательные компоненты Dice/Rigidbody.", this); + return; + } + if (_rollRoutine != null) StopCoroutine(_rollRoutine); @@ -80,12 +127,17 @@ namespace YachtDice.Dice private IEnumerator RollSequence() { IsRolling = true; + CaptureStartPose(); // ── 1. Подготовка ──────────────────────────────────────────── + if (diceCollider != null) + diceCollider.enabled = true; + rb.isKinematic = false; rb.useGravity = true; rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; + var rollStartedAt = Time.time; // ── 2. Импульс: подбросить вверх с лёгким разбросом ───────── Vector3 force = new Vector3( @@ -105,9 +157,17 @@ namespace YachtDice.Dice // ── 4. Ждём пока кубик успокоится ─────────────────────────── var stillTimer = 0f; var sqrThreshold = settleSpeed * settleSpeed; + var didTimeout = false; + var maxDuration = Mathf.Max(0.1f, maxRollDuration); while (stillTimer < settleDelay) { + if (Time.time - rollStartedAt >= maxDuration) + { + didTimeout = true; + break; + } + yield return new WaitForFixedUpdate(); bool isSlow = rb.linearVelocity.sqrMagnitude < sqrThreshold @@ -116,6 +176,9 @@ namespace YachtDice.Dice stillTimer = isSlow ? stillTimer + Time.fixedDeltaTime : 0f; } + if (didTimeout) + Debug.LogWarning($"DiceRoller: бросок принудительно завершён по таймауту ({maxDuration:0.##} c).", this); + // ── 5. Замораживаем физику и читаем верхнюю грань ─────────── rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; @@ -124,8 +187,7 @@ namespace YachtDice.Dice if (!dice.TryGetTopValue(out int topValue)) { Debug.LogWarning("DiceRoller: не удалось определить верхнюю грань."); - IsRolling = false; - yield break; + topValue = 1; } // ── 6. Плавный доворот до ровного положения ───────────────── @@ -139,20 +201,73 @@ namespace YachtDice.Dice transform.rotation = startRot; var elapsed = 0f; - while (elapsed < snapDuration) + var snapTime = Mathf.Max(0.01f, snapDuration); + var snapCurveToUse = snapCurve ?? AnimationCurve.Linear(0f, 0f, 1f, 1f); + while (elapsed < snapTime) { elapsed += Time.deltaTime; - float t = snapCurve.Evaluate(Mathf.Clamp01(elapsed / snapDuration)); + float t = snapCurveToUse.Evaluate(Mathf.Clamp01(elapsed / snapTime)); transform.rotation = Quaternion.Slerp(startRot, targetRot, t); yield return null; } transform.rotation = targetRot; - // ── 7. Готово ─────────────────────────────────────────────── + // ── 7. Возвращаемся в стартовую позицию без коллизий ─────── + if (diceCollider != null) + diceCollider.enabled = false; + + yield return ReturnToStartPosition(); + + if (diceCollider != null) + diceCollider.enabled = true; + + // ── 8. Готово ─────────────────────────────────────────────── IsRolling = false; OnRollFinished?.Invoke(topValue); // Debug.Log($"{gameObject.name} | Выпало: {topValue}"); } + + private IEnumerator ReturnToStartPosition() + { + var startPos = transform.localPosition; + var duration = Mathf.Max(0.01f, returnDuration); + + yield return WaitForReturnTurn(); + + try + { + var elapsed = 0f; + var returnCurveToUse = returnCurve ?? AnimationCurve.Linear(0f, 0f, 1f, 1f); + while (elapsed < duration) + { + elapsed += Time.deltaTime; + + float normalized = Mathf.Clamp01(elapsed / duration); + float curved = returnCurveToUse.Evaluate(normalized); + + var pos = Vector3.Lerp(startPos, _startLocalPosition, curved); + pos.y += Mathf.Sin(normalized * Mathf.PI) * returnArcHeight; + + transform.localPosition = pos; + yield return null; + } + + transform.localPosition = _startLocalPosition; + } + finally + { + if (_activeReturnRoller == this) + _activeReturnRoller = null; + } + } + + private IEnumerator WaitForReturnTurn() + { + while (_activeReturnRoller != null && _activeReturnRoller != this) + yield return null; + + _activeReturnRoller = this; + } } }