349 lines
12 KiB
C#
349 lines
12 KiB
C#
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
|
||
}
|