using UnityEngine; using UnityEngine.SceneManagement; [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Collider2D))] public class PickableProjectile : BasicProjectile { public static PickableProjectile Instance { get; private set; } public enum State { Held, Airborne, Grounded, Returning } public State CurrentState { get; private set; } = State.Grounded; [Header("Pickup/Attach")] public Vector3 heldLocalOffset = new(0.25f, 0.1f, 0f); public float ignoreOwnerTimeOnThrow = 0.25f; 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("Persistence / Recall")] [Tooltip("Сохранять снаряд между сценами.")] public bool persistAcrossScenes = true; [Tooltip("Если перед загрузкой новой сцены снаряд был в руке — автоматически прицепиться к игроку на новой сцене.")] public bool reattachIfWasHeldOnLoad = true; [Tooltip("Старт возврата, если дальше этой дистанции от точки руки.")] public float recallStartDistance = 8f; [Tooltip("На этой дистанции от руки считаем, что прилетел и подбираем.")] public float recallArriveDistance = 1.4f; [Tooltip("Максимальная скорость «магнита».")] public float recallMaxSpeed = 16f; [Tooltip("Ускорение «магнита».")] public float recallAcceleration = 60f; [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; // «Когда-либо был в руке» — разрешает бумеранг private bool hadBeenHeld = false; // Запоминаем последнего игрока для возврата private GameObject lastKnownHolder; protected override void Awake() { base.Awake(); if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; if (persistAcrossScenes) DontDestroyOnLoad(gameObject); SceneManager.sceneLoaded += OnSceneLoaded; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; col.isTrigger = false; BecomeGrounded(null); } private void OnDestroy() { if (Instance == this) Instance = null; SceneManager.sceneLoaded -= OnSceneLoaded; } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { Debug.Log("OnSceneLoaded"); Debug.Log($"Owner: {owner}"); Debug.Log($"LastKnownHolder: {lastKnownHolder}"); if (!reattachIfWasHeldOnLoad) return; // Если до загрузки мы были в руке — прицепляемся к игроку на новой сцене if (CurrentState == State.Held || (hadBeenHeld && owner != null)) { var playerGo = (GameManager.Instance && GameManager.Instance.playerObj) ? GameManager.Instance.playerObj : GameObject.FindWithTag("Player"); Debug.Log($"PlayerObj: {playerGo}"); if (!playerGo) return; var attach = FindAttachPointOn(playerGo) ?? playerGo.transform; transform.SetParent(null, true); TryPickup(playerGo, attach); } } public bool TryPickup(GameObject newHolder, Transform optionalAttachPoint = null) { if (CurrentState == State.Held) return true; holder = newHolder; lastKnownHolder = 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; hadBeenHeld = true; // разрешаем бумеранг в будущем return true; } public void Launch(Vector2 direction, float speed, GameObject throwOwner = null) { if (CurrentState == State.Held) transform.SetParent(null, true); holder = throwOwner; lastKnownHolder = 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 (hadBeenHeld && CurrentState != State.Held) HandleRecallToPlayer(); } private void HandleRecallToPlayer() { // Целимся в актуального игрока, предпочтительно последнего владельца GameObject playerGo = null; if (lastKnownHolder != null) playerGo = lastKnownHolder; if (playerGo == null || !playerGo.activeInHierarchy) playerGo = (GameManager.Instance && GameManager.Instance.playerObj) ? GameManager.Instance.playerObj : GameObject.FindWithTag("Player"); if (!playerGo) return; var hand = FindAttachPointOn(playerGo) ?? playerGo.transform; Vector2 toHand = (Vector2)hand.position - rb.position; float dist = toHand.magnitude; if (dist > recallStartDistance) { // Начинаем «магнит» if (CurrentState != State.Returning) { CurrentState = State.Returning; rb.bodyType = RigidbodyType2D.Dynamic; col.enabled = true; // ВАЖНО: не игнорируем коллизию с игроком здесь, чтобы не проскальзывать через него } Vector2 desired = toHand.normalized * recallMaxSpeed; rb.linearVelocity = Vector2.MoveTowards( rb.linearVelocity, desired, recallAcceleration * Time.fixedDeltaTime); if (rb.linearVelocity.sqrMagnitude > 0.01f) transform.right = rb.linearVelocity.normalized; if (dist <= recallArriveDistance) TryPickup(playerGo, hand); } else { // Вышли из «дальней» зоны — возвращаем обычное состояние if (CurrentState == State.Returning) CurrentState = rb.bodyType == RigidbodyType2D.Dynamic ? State.Airborne : State.Grounded; } } #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 }