Danro/Assets/Resources/Scripts/Projectiles/PickableProjectile.cs

349 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}