Danro/Assets/Resources/Scripts/PickableProjectile.cs

247 lines
7.2 KiB
C#

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
}