[Fix] Code visual
This commit is contained in:
+115
-116
@@ -4,138 +4,137 @@ using UnityEngine;
|
||||
|
||||
namespace YachtDice.Dice
|
||||
{
|
||||
|
||||
public sealed class Dice : MonoBehaviour
|
||||
{
|
||||
[Serializable]
|
||||
public struct Entry : IEquatable<Entry>
|
||||
public class Dice : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int value;
|
||||
[SerializeField] private Transform point;
|
||||
|
||||
public int Value => value;
|
||||
public Transform Point => point;
|
||||
|
||||
public bool Equals(Entry other) => point == other.point;
|
||||
public override bool Equals(object obj) => obj is Entry other && Equals(other);
|
||||
public override int GetHashCode() => point != null ? point.GetHashCode() : 0;
|
||||
}
|
||||
|
||||
[SerializeField] private List<Entry> entries = new();
|
||||
|
||||
private HashSet<Entry> entrySet;
|
||||
|
||||
private void Awake() => RebuildSet();
|
||||
private void OnValidate() => RebuildSet();
|
||||
|
||||
public void Test()
|
||||
{
|
||||
if (!TryGetTopValue(out var top) || !TryGetBottomValue(out var bottom))
|
||||
return;
|
||||
|
||||
Debug.Log($"Top Value: {top}, Bottom Value: {bottom}");
|
||||
|
||||
AlignToTopByLocalAngles();
|
||||
}
|
||||
|
||||
private void RebuildSet()
|
||||
{
|
||||
entrySet = new HashSet<Entry>();
|
||||
if (entries == null) return;
|
||||
for (int i = 0; i < entries.Count; i++) entrySet.Add(entries[i]);
|
||||
}
|
||||
|
||||
public bool TryGetTopValue(out int value) => TryGetExtremeValue(isTop: true, out value);
|
||||
public bool TryGetBottomValue(out int value) => TryGetExtremeValue(isTop: false, out value);
|
||||
|
||||
public int AlignToTopByLocalAngles()
|
||||
{
|
||||
var e = GetExtremeEntryByWorldY(isTop: true);
|
||||
AlignByFaceLocalAngles(e.Point);
|
||||
return e.Value;
|
||||
}
|
||||
|
||||
public int AlignToBottomByLocalAngles()
|
||||
{
|
||||
var e = GetExtremeEntryByWorldY(isTop: false);
|
||||
AlignByFaceLocalAngles(e.Point);
|
||||
return e.Value;
|
||||
}
|
||||
|
||||
private bool TryGetExtremeValue(bool isTop, out int value)
|
||||
{
|
||||
value = default;
|
||||
|
||||
if (entrySet == null || entrySet.Count == 0)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
float bestY = isTop ? float.NegativeInfinity : float.PositiveInfinity;
|
||||
int best = default;
|
||||
|
||||
foreach (var e in entrySet)
|
||||
[Serializable]
|
||||
public struct Entry : IEquatable<Entry>
|
||||
{
|
||||
var p = e.Point;
|
||||
if (!p) continue;
|
||||
[SerializeField] private int value;
|
||||
[SerializeField] private Transform point;
|
||||
|
||||
float y = p.position.y;
|
||||
if (!found || (isTop ? y > bestY : y < bestY))
|
||||
{
|
||||
found = true;
|
||||
bestY = y;
|
||||
best = e.Value;
|
||||
}
|
||||
public int Value => value;
|
||||
public Transform Point => point;
|
||||
|
||||
public bool Equals(Entry other) => point == other.point;
|
||||
public override bool Equals(object obj) => obj is Entry other && Equals(other);
|
||||
public override int GetHashCode() => point != null ? point.GetHashCode() : 0;
|
||||
}
|
||||
|
||||
if (!found) return false;
|
||||
[SerializeField] private List<Entry> entries = new();
|
||||
|
||||
value = best;
|
||||
return true;
|
||||
}
|
||||
private HashSet<Entry> entrySet;
|
||||
|
||||
private Entry GetExtremeEntryByWorldY(bool isTop)
|
||||
{
|
||||
if (entrySet == null || entrySet.Count == 0)
|
||||
throw new InvalidOperationException("Dice: коллекция пуста.");
|
||||
private void Awake() => RebuildSet();
|
||||
private void OnValidate() => RebuildSet();
|
||||
|
||||
bool found = false;
|
||||
float bestY = isTop ? float.NegativeInfinity : float.PositiveInfinity;
|
||||
Entry best = default;
|
||||
|
||||
foreach (var e in entrySet)
|
||||
public void Test()
|
||||
{
|
||||
var p = e.Point;
|
||||
if (!p) continue;
|
||||
if (!TryGetTopValue(out var top) || !TryGetBottomValue(out var bottom))
|
||||
return;
|
||||
|
||||
float y = p.position.y;
|
||||
if (!found || (isTop ? y > bestY : y < bestY))
|
||||
{
|
||||
found = true;
|
||||
bestY = y;
|
||||
best = e;
|
||||
}
|
||||
Debug.Log($"Top Value: {top}, Bottom Value: {bottom}");
|
||||
|
||||
AlignToTopByLocalAngles();
|
||||
}
|
||||
|
||||
if (!found)
|
||||
throw new InvalidOperationException("Dice: все Transform == null.");
|
||||
private void RebuildSet()
|
||||
{
|
||||
entrySet = new HashSet<Entry>();
|
||||
if (entries == null) return;
|
||||
for (int i = 0; i < entries.Count; i++) entrySet.Add(entries[i]);
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
public bool TryGetTopValue(out int value) => TryGetExtremeValue(isTop: true, out value);
|
||||
public bool TryGetBottomValue(out int value) => TryGetExtremeValue(isTop: false, out value);
|
||||
|
||||
private static float Norm180(float a)
|
||||
{
|
||||
a %= 360f;
|
||||
return a > 180f ? a - 360f : (a < -180f ? a + 360f : a);
|
||||
}
|
||||
public int AlignToTopByLocalAngles()
|
||||
{
|
||||
var e = GetExtremeEntryByWorldY(isTop: true);
|
||||
AlignByFaceLocalAngles(e.Point);
|
||||
return e.Value;
|
||||
}
|
||||
|
||||
private void AlignByFaceLocalAngles(Transform facePoint)
|
||||
{
|
||||
if (!facePoint) throw new InvalidOperationException("Dice: facePoint == null.");
|
||||
public int AlignToBottomByLocalAngles()
|
||||
{
|
||||
var e = GetExtremeEntryByWorldY(isTop: false);
|
||||
AlignByFaceLocalAngles(e.Point);
|
||||
return e.Value;
|
||||
}
|
||||
|
||||
transform.localEulerAngles = Vector3.Scale(facePoint.localEulerAngles, new Vector3(-1f, -1f, -1f));
|
||||
private bool TryGetExtremeValue(bool isTop, out int value)
|
||||
{
|
||||
value = default;
|
||||
|
||||
var e = transform.localEulerAngles;
|
||||
transform.localEulerAngles = new Vector3(Norm180(e.x), Norm180(e.y), Norm180(e.z));
|
||||
if (entrySet == null || entrySet.Count == 0)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
float bestY = isTop ? float.NegativeInfinity : float.PositiveInfinity;
|
||||
int best = default;
|
||||
|
||||
foreach (var e in entrySet)
|
||||
{
|
||||
var p = e.Point;
|
||||
if (!p) continue;
|
||||
|
||||
float y = p.position.y;
|
||||
if (!found || (isTop ? y > bestY : y < bestY))
|
||||
{
|
||||
found = true;
|
||||
bestY = y;
|
||||
best = e.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) return false;
|
||||
|
||||
value = best;
|
||||
return true;
|
||||
}
|
||||
|
||||
private Entry GetExtremeEntryByWorldY(bool isTop)
|
||||
{
|
||||
if (entrySet == null || entrySet.Count == 0)
|
||||
throw new InvalidOperationException("Dice: коллекция пуста.");
|
||||
|
||||
bool found = false;
|
||||
float bestY = isTop ? float.NegativeInfinity : float.PositiveInfinity;
|
||||
Entry best = default;
|
||||
|
||||
foreach (var e in entrySet)
|
||||
{
|
||||
var p = e.Point;
|
||||
if (!p) continue;
|
||||
|
||||
float y = p.position.y;
|
||||
if (!found || (isTop ? y > bestY : y < bestY))
|
||||
{
|
||||
found = true;
|
||||
bestY = y;
|
||||
best = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
throw new InvalidOperationException("Dice: все Transform == null.");
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static float Norm180(float a)
|
||||
{
|
||||
a %= 360f;
|
||||
return a > 180f ? a - 360f : (a < -180f ? a + 360f : a);
|
||||
}
|
||||
|
||||
private void AlignByFaceLocalAngles(Transform facePoint)
|
||||
{
|
||||
if (!facePoint) throw new InvalidOperationException("Dice: facePoint == null.");
|
||||
|
||||
transform.localEulerAngles = Vector3.Scale(facePoint.localEulerAngles, new Vector3(-1f, -1f, -1f));
|
||||
|
||||
var e = transform.localEulerAngles;
|
||||
transform.localEulerAngles = new Vector3(Norm180(e.x), Norm180(e.y), Norm180(e.z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+126
-127
@@ -5,152 +5,151 @@ using Random = UnityEngine.Random;
|
||||
|
||||
namespace YachtDice.Dice
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится,
|
||||
/// плавно доворачивает до ровного положения и сообщает результат.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение.
|
||||
/// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится,
|
||||
/// плавно доворачивает до ровного положения и сообщает результат.
|
||||
/// </summary>
|
||||
public event Action<int> OnRollFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Идёт ли сейчас бросок.
|
||||
/// </summary>
|
||||
public bool IsRolling { get; private set; }
|
||||
|
||||
private Coroutine rollRoutine;
|
||||
|
||||
private void Reset()
|
||||
public class DiceRoller : MonoBehaviour
|
||||
{
|
||||
dice = GetComponent<Dice>();
|
||||
rb = GetComponent<Rigidbody>();
|
||||
}
|
||||
[Header("References")]
|
||||
[SerializeField] private Dice dice;
|
||||
[SerializeField] private Rigidbody rb;
|
||||
|
||||
[Header("Throw Settings")]
|
||||
[Tooltip("Сила подброса вверх")]
|
||||
[SerializeField] private float throwUpForce = 6f;
|
||||
|
||||
/// <summary>
|
||||
/// Бросить кубик. Повторный вызов во время броска игнорируется.
|
||||
/// </summary>
|
||||
public void Roll()
|
||||
{
|
||||
if (IsRolling) return;
|
||||
[Tooltip("Горизонтальный разброс")]
|
||||
[SerializeField] private float throwScatter = 1.5f;
|
||||
|
||||
if (rollRoutine != null)
|
||||
StopCoroutine(rollRoutine);
|
||||
[Tooltip("Минимальный крутящий момент")]
|
||||
[SerializeField] private float torqueMin = 15f;
|
||||
|
||||
rollRoutine = StartCoroutine(RollSequence());
|
||||
}
|
||||
|
||||
private IEnumerator RollSequence()
|
||||
{
|
||||
IsRolling = true;
|
||||
[Tooltip("Максимальный крутящий момент")]
|
||||
[SerializeField] private float torqueMax = 30f;
|
||||
|
||||
// ── 1. Подготовка ────────────────────────────────────────────
|
||||
rb.isKinematic = false;
|
||||
rb.useGravity = true;
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
[Header("Settle Detection")]
|
||||
[Tooltip("Порог скорости, ниже которого кубик считается остановившимся")]
|
||||
[SerializeField] private float settleSpeed = 0.05f;
|
||||
|
||||
// ── 2. Импульс: подбросить вверх с лёгким разбросом ─────────
|
||||
Vector3 force = new Vector3(
|
||||
Random.Range(-throwScatter, throwScatter),
|
||||
throwUpForce,
|
||||
Random.Range(-throwScatter, throwScatter)
|
||||
);
|
||||
rb.AddForce(force, ForceMode.Impulse);
|
||||
[Tooltip("Сколько секунд кубик должен быть неподвижен, чтобы засчитать остановку")]
|
||||
[SerializeField] private float settleDelay = 0.3f;
|
||||
|
||||
// ── 3. Случайный крутящий момент — красивое вращение ────────
|
||||
Vector3 torque = Random.onUnitSphere * Random.Range(torqueMin, torqueMax);
|
||||
rb.AddTorque(torque, ForceMode.Impulse);
|
||||
[Header("Snap Alignment")]
|
||||
[Tooltip("Длительность плавного доворота к ровному положению")]
|
||||
[SerializeField] private float snapDuration = 0.35f;
|
||||
|
||||
// Даём кубику взлететь
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
[SerializeField]
|
||||
private AnimationCurve snapCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение.
|
||||
/// </summary>
|
||||
public event Action<int> OnRollFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Идёт ли сейчас бросок.
|
||||
/// </summary>
|
||||
public bool IsRolling { get; private set; }
|
||||
|
||||
// ── 4. Ждём пока кубик успокоится ───────────────────────────
|
||||
float stillTimer = 0f;
|
||||
float sqrThreshold = settleSpeed * settleSpeed;
|
||||
private Coroutine rollRoutine;
|
||||
|
||||
while (stillTimer < settleDelay)
|
||||
private void Reset()
|
||||
{
|
||||
yield return new WaitForFixedUpdate();
|
||||
|
||||
bool isSlow = rb.linearVelocity.sqrMagnitude < sqrThreshold
|
||||
&& rb.angularVelocity.sqrMagnitude < sqrThreshold;
|
||||
|
||||
stillTimer = isSlow ? stillTimer + Time.fixedDeltaTime : 0f;
|
||||
dice = GetComponent<Dice>();
|
||||
rb = GetComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
// ── 5. Замораживаем физику и читаем верхнюю грань ───────────
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
rb.isKinematic = true;
|
||||
|
||||
if (!dice.TryGetTopValue(out int topValue))
|
||||
/// <summary>
|
||||
/// Бросить кубик. Повторный вызов во время броска игнорируется.
|
||||
/// </summary>
|
||||
public void Roll()
|
||||
{
|
||||
Debug.LogWarning("DiceRoller: не удалось определить верхнюю грань.");
|
||||
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;
|
||||
yield break;
|
||||
OnRollFinished?.Invoke(topValue);
|
||||
|
||||
// Debug.Log($"{gameObject.name} | Выпало: <b>{topValue}</b>");
|
||||
}
|
||||
|
||||
// ── 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} | Выпало: <b>{topValue}</b>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user