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

390 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;
[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 bool snapExactlyToAttachPoint = true;
public float ignoreOwnerTimeOnThrow = 0.25f;
public string attachPointChildName = "HandPoint";
[Header("Physics on ground")]
public float sleepVelocity = 0.05f;
public bool lockWhenLanded = true;
[Header("Bouncing")]
public int maxBounces = 3;
public float minBounceSpeed = 0.8f;
[Range(0f, 1f)] public float restitution = 0.6f;
[Range(0f, 1f)] public float tangentDamping = 0.85f;
[Range(0f, 1f)] public float groundUpTolerance = 0.25f;
public float stickDelayAfterImpact = 0.03f;
[Tooltip("Старт возврата, если дальше этой дистанции от точки руки.")]
public float recallStartDistance = 8f;
[Tooltip("На этой дистанции от руки считаем, что прилетел и подбираем.")]
public float recallArriveDistance = 1f;
public float recallSpeed = 16f;
public float recallAcceleration = 60f;
[Header("Gizmos")]
public bool drawGizmos;
public Color gizmoColor = new Color(0.2f, 0.8f, 1f, 0.35f);
private Transform holderAttachPoint;
private int bounceCounter = 0;
private float lastImpactTime = -999f;
private Vector3 baseLocalScale = Vector3.one;
private bool hadBeenHeld = false;
private GameObject lastKnownHolder;
protected override void Awake()
{
base.Awake();
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
private void OnDestroy()
{
if (Instance == this) Instance = null;
}
private void Start()
{
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
col.isTrigger = false;
if (CurrentState != State.Held)
BecomeGrounded(null);
}
protected override void FixedUpdate()
{
base.FixedUpdate();
if (!lockWhenLanded && CurrentState == State.Airborne)
{
if (rb.linearVelocity.sqrMagnitude < (sleepVelocity * sleepVelocity))
BecomeGrounded(null);
}
if ((CurrentState == State.Airborne || CurrentState == State.Returning) && rb.linearVelocity.sqrMagnitude > 0.0001f)
transform.up = rb.linearVelocity.normalized;
if (CurrentState == State.Airborne && rb.linearVelocity.magnitude < 0.1f)
BecomeGrounded(null);
if (hadBeenHeld && CurrentState != State.Held)
HandleRecallToPlayer();
}
protected override void OnCollisionEnter2D(Collision2D collision)
{
var otherRb = collision.rigidbody ? collision.rigidbody : collision.otherRigidbody;
var otherGo = (collision.collider ? collision.collider.attachedRigidbody?.gameObject : null)
?? otherRb?.gameObject
?? collision.gameObject;
if (otherGo && otherGo.CompareTag("Player") && CurrentState != State.Held)
{
if (TryPickup(otherGo)) return;
}
base.OnCollisionEnter2D(collision);
if (CurrentState == State.Airborne)
{
if (TryBounce(collision))
{
lastImpactTime = Time.time;
return;
}
BecomeGrounded(collision);
}
}
public bool TryPickup(GameObject newHolder, Transform optionalAttachPoint = null)
{
if (CurrentState == State.Held) return true;
owner = newHolder;
lastKnownHolder = newHolder;
holderAttachPoint = optionalAttachPoint ?? FindAttachPoint(newHolder);
var fireScript = newHolder.GetComponent<PlayerFireScript>();
if (holderAttachPoint == null && fireScript != null)
holderAttachPoint = fireScript.attachPoint;
if (holderAttachPoint == null)
holderAttachPoint = newHolder.transform;
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
rb.bodyType = RigidbodyType2D.Kinematic;
col.enabled = false;
transform.SetParent(holderAttachPoint, worldPositionStays: true);
transform.position = holderAttachPoint.TransformPoint(heldLocalOffset);
transform.rotation = holderAttachPoint.rotation;
transform.localScale = baseLocalScale;
if (fireScript != null) fireScript.BindProjectile(this);
StartCoroutine(IgnoreOwnerTemporarily(owner, ignoreOwnerTimeOnThrow));
CurrentState = State.Held;
launched = false;
hadBeenHeld = true;
return true;
}
private Transform ResolveAttachPoint(GameObject holder, Transform preferred = null)
{
if (preferred) return preferred;
var fs = holder.GetComponent<PlayerFireScript>();
if (fs && fs.attachPoint) return fs.attachPoint;
return holder.transform;
}
public void Launch(Vector2 direction, float speed, GameObject throwOwner = null)
{
if (CurrentState == State.Held)
transform.SetParent(null, worldPositionStays: true);
owner = throwOwner;
lastKnownHolder = throwOwner;
holderAttachPoint = null;
rb.bodyType = RigidbodyType2D.Dynamic;
col.enabled = true;
launched = true;
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.up = dir;
transform.localScale = baseLocalScale;
if (throwOwner != null && ignoreOwnerTimeOnThrow > 0f)
StartCoroutine(IgnoreOwnerTemporarily(throwOwner, ignoreOwnerTimeOnThrow));
CurrentState = State.Airborne;
bounceCounter = 0;
}
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;
Vector2 vIn = rb.linearVelocity;
float speed = vIn.magnitude;
if (speed < minBounceSpeed) return false;
Vector2 n = Vector2.up;
float wsum = 0f;
float minSeparation = float.PositiveInfinity;
if (collision.contactCount > 0)
{
Vector2 sum = Vector2.zero;
for (int i = 0; i < collision.contactCount; i++)
{
var cp = collision.GetContact(i);
float w = Mathf.Max(0.0001f, Mathf.Abs(cp.normalImpulse));
sum += cp.normal * w;
wsum += w;
if (cp.separation < minSeparation) minSeparation = cp.separation;
}
n = (wsum > 0f ? sum / wsum : sum);
if (n.sqrMagnitude < 1e-6f) n = Vector2.up;
else n.Normalize();
}
else
{
n = Vector2.up;
minSeparation = 0f;
}
if (Vector2.Dot(vIn, n) > 0f) n = -n;
float matBounciness = restitution;
float frA = 0f, frB = 0f;
var myColMat = col ? col.sharedMaterial : null;
if (myColMat)
{
matBounciness = Mathf.Max(matBounciness, myColMat.bounciness);
frA = Mathf.Clamp01(myColMat.friction);
}
var otherCol = collision.collider;
if (otherCol)
{
var otherMat = otherCol.sharedMaterial;
if (otherMat)
{
matBounciness = Mathf.Max(matBounciness, otherMat.bounciness);
frB = Mathf.Clamp01(otherMat.friction);
}
}
float tangentFactor = (1f - frA) * (1f - frB) * Mathf.Clamp01(tangentDamping);
Vector2 vN = Vector2.Dot(vIn, n) * n;
Vector2 vT = vIn - vN;
Vector2 vOut = (-vN) * matBounciness + vT * tangentFactor;
rb.linearVelocity = vOut;
float pushOut = 0.005f;
if (minSeparation < 0f) pushOut = Mathf.Max(pushOut, -minSeparation + 0.001f);
rb.position += n * pushOut;
if (vOut.sqrMagnitude > 1e-6f)
transform.up = vOut.normalized;
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 FindAttachPoint(GameObject who)
{
if (string.IsNullOrWhiteSpace(attachPointChildName)) return null;
var t = who.transform.Find(attachPointChildName);
return t;
}
private void HandleRecallToPlayer()
{
GameObject playerGo = lastKnownHolder;
if (playerGo == null || !playerGo.activeInHierarchy)
playerGo = (GameManager.Instance && GameManager.Instance.PlayerObj)
? GameManager.Instance.PlayerObj
: GameObject.FindWithTag("Player");
if (!playerGo) return;
var attachPoint = ResolveAttachPoint(playerGo);
bool isVisible = false;
var mainCamera = Camera.main;
float margin = 0.1f;
if (mainCamera)
{
Vector3 vp = mainCamera.WorldToViewportPoint(transform.position);
isVisible = vp.x >= -margin && vp.x <= 1f + margin
&& vp.y >= -margin && vp.y <= 1f + margin;
}
else
{
Vector2 toOwnerTest = (Vector2)attachPoint.position - rb.position;
isVisible = (toOwnerTest.magnitude <= recallStartDistance);
}
Vector2 toOwner = (Vector2)attachPoint.position - rb.position;
float dist = toOwner.magnitude;
if (CurrentState != State.Returning)
{
if (!isVisible)
{
CurrentState = State.Returning;
rb.bodyType = RigidbodyType2D.Dynamic;
col.enabled = false;
}
else
{
return;
}
}
//float speed = rb.linearVelocity.magnitude;
//if (speed < 8f) speed = recallSpeed;
Vector2 dir = (dist > 0.001f) ? (toOwner / dist) : rb.linearVelocity.normalized;
rb.linearVelocity = dir * recallSpeed; //* speed;
if (rb.linearVelocity.sqrMagnitude > 0.01f)
transform.up = rb.linearVelocity.normalized;
if (dist <= recallArriveDistance)
TryPickup(playerGo);
}
#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
}