using UnityEngine; [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Collider2D))] public class PickableProjectile : BasicProjectile { public static PickableProjectile Instance { get; private set; } public enum State { Held, Airborne, Grounded } public State CurrentState { get; private set; } = State.Grounded; [Header("Pickup/Attach")] public Vector3 heldLocalOffset = new Vector3(0.25f, 0.1f, 0f); public float ignoreOwnerTimeOnThrow = 0.15f; public string attachPointChildName = "HandPoint"; [Header("Physics on ground")] public float sleepVelocity = 0.05f; public bool lockWhenLanded = true; [Header("Bouncing")] [Tooltip("Максимум отскоков прежде чем лечь на землю.")] public int maxBounces = 3; [Tooltip("Минимальная скорость для отскока.")] public float minBounceSpeed = 0.8f; [Tooltip("Коэффициент упругости (0..1).")] [Range(0f, 1f)] public float restitution = 0.6f; [Tooltip("Демпфирование вдоль поверхности (0..1), меньше — сильнее гасит скольжение.")] [Range(0f, 1f)] public float tangentDamping = 0.85f; [Tooltip("Насколько «вверх» должна смотреть средняя нормаль, чтобы считать поверхность землёй (чем больше, тем мягче фильтр).")] [Range(0f, 1f)] public float groundUpTolerance = 0.25f; [Tooltip("Маленькая пауза, чтобы не прилипнуть в кадр столкновения.")] public float stickDelayAfterImpact = 0.03f; [Header("Gizmos")] public bool drawGizmos; public Color gizmoColor = new Color(0.2f, 0.8f, 1f, 0.35f); private GameObject holder; private Transform holderAttachPoint; private int bounceCounter = 0; private float lastImpactTime = -999f; protected override void Awake() { base.Awake(); if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; col.isTrigger = false; BecomeGrounded(null); } private void OnDestroy() { if (Instance == this) Instance = null; } public bool TryPickup(GameObject newHolder, Transform optionalAttachPoint = null) { if (CurrentState == State.Held) return true; holder = newHolder; holderAttachPoint = optionalAttachPoint ?? FindAttachPointOn(holder); if (holderAttachPoint == null) holderAttachPoint = holder.transform; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.bodyType = RigidbodyType2D.Kinematic; col.enabled = false; transform.SetParent(holderAttachPoint, worldPositionStays: false); transform.localPosition = heldLocalOffset; transform.localRotation = Quaternion.identity; transform.localScale = Vector3.one * 2; StartCoroutine(IgnoreOwnerTemporarily(holder, ignoreOwnerTimeOnThrow)); CurrentState = State.Held; launched = false; owner = holder; return true; } public void Launch(Vector2 direction, float speed, GameObject throwOwner = null) { if (CurrentState == State.Held) { transform.SetParent(null, true); } holder = throwOwner; holderAttachPoint = null; rb.bodyType = RigidbodyType2D.Dynamic; col.enabled = true; launched = true; owner = throwOwner; Vector2 dir = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.right; float finalSpeed = (speed > 0f) ? speed : initialSpeed; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.AddForce(dir * finalSpeed * rb.mass, ForceMode2D.Impulse); transform.right = dir; if (throwOwner != null && ignoreOwnerTimeOnThrow > 0f) StartCoroutine(IgnoreOwnerTemporarily(throwOwner, ignoreOwnerTimeOnThrow)); CurrentState = State.Airborne; bounceCounter = 0; } protected override void OnCollisionEnter2D(Collision2D collision) { base.OnCollisionEnter2D(collision); if (CurrentState == State.Airborne) { if (TryBounce(collision)) { lastImpactTime = Time.time; return; } BecomeGrounded(collision); } } protected override void OnImpactFinal(Collision2D collision) { if (!TryBounce(collision)) BecomeGrounded(collision); } protected override void OnImpactFinal(Collider2D other) { if (Time.time - lastImpactTime > stickDelayAfterImpact) BecomeGrounded(null); } private bool TryBounce(Collision2D collision) { if (Time.time - lastImpactTime < stickDelayAfterImpact) return true; var v = rb.linearVelocity; float speed = v.magnitude; if (speed < minBounceSpeed) return false; Vector2 avgNormal = Vector2.up; if (collision.contactCount > 0) { avgNormal = Vector2.zero; for (int i = 0; i < collision.contactCount; i++) avgNormal += collision.GetContact(i).normal; avgNormal.Normalize(); } Vector2 reflected = Vector2.Reflect(v, avgNormal) * restitution; Vector2 n = avgNormal; Vector2 vn = Vector2.Dot(reflected, n) * n; Vector2 vt = reflected - vn; reflected = vn + vt * tangentDamping; rb.linearVelocity = reflected; rb.position += n * 0.005f; bounceCounter++; if (bounceCounter >= maxBounces) return false; CurrentState = State.Airborne; return true; } private void BecomeGrounded(Collision2D collision) { launched = false; CurrentState = State.Grounded; if (lockWhenLanded) { rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.bodyType = RigidbodyType2D.Kinematic; } else { rb.bodyType = RigidbodyType2D.Dynamic; } col.enabled = true; transform.SetParent(null, true); bounceCounter = 0; } private Transform FindAttachPointOn(GameObject who) { if (string.IsNullOrWhiteSpace(attachPointChildName)) return null; var t = who.transform.Find(attachPointChildName); return t; } protected override void FixedUpdate() { base.FixedUpdate(); if (!lockWhenLanded && CurrentState == State.Airborne) { if (rb.linearVelocity.sqrMagnitude < (sleepVelocity * sleepVelocity)) BecomeGrounded(null); } } #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (!drawGizmos) return; Gizmos.color = gizmoColor; Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawCube(new Vector3(heldLocalOffset.x, heldLocalOffset.y, 0), new Vector3(0.12f, 0.12f, 0.02f)); } #endif }