using UnityEngine;
using RR.Game2D.Core;
using RR.Game2D.Behaviour;
namespace RR.Game2D.Entities.Projectiles
{
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody2D),
typeof(Collider2D))]
public class PickableProjectile : Projectile2D
{
public static PickableProjectile Instance { get; private set; }
public enum State { Held, Airborne, Grounded, Returning }
public State CurrentState { get; private set; } = State.Grounded;
[Header("Подбор и прикрепление")]
[SerializeField] private Vector3 heldLocalOffset = new(0.25f, 0.1f, 0f);
[SerializeField] private float heldScale = 1.5f;
[SerializeField] private float ignoreOwnerTimeOnThrow = 0.1f;
[SerializeField] private string attachPointChildName = "HandPoint";
[Header("Приземление")]
[SerializeField] private float groundVelocity = 0.05f;
[SerializeField] private bool lockWhenLanded = true;
[Header("Отскоки")]
[SerializeField] private int maxBounces = 3;
[SerializeField] private float minBounceSpeed = 0.8f;
[SerializeField, Range(0f, 1f)] private float restitution = 0.6f;
[SerializeField, Range(0f, 1f)] private float tangentDamping = 0.85f;
[SerializeField] private float stickDelayAfterImpact = 0.03f;
[Tooltip("Старт возврата, если дальше этой дистанции от точки руки.")]
[SerializeField] private float recallStartDistance = 8f;
[Tooltip("На этой дистанции от руки считаем, что прилетел и подбираем.")]
[SerializeField] private float recallArriveDistance = 1f;
[SerializeField] private float recallSpeed = 16f;
[Header("Gizmos")]
[SerializeField] private bool drawGizmos;
[SerializeField] private 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;
//---------------------------
// Singleton
//---------------------------
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);
}
//private void Update()
//{
// Debug.Log(CurrentState);
//}
protected override void FixedUpdate()
{
base.FixedUpdate();
if (!lockWhenLanded && CurrentState == State.Airborne)
{
if (rb.linearVelocity.sqrMagnitude < (groundVelocity * groundVelocity))
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;
else
BecomeGrounded(collision);
}
}
///
/// Метод для подбора снаряда игроком.
/// Привязывает снаряд к точке привязки на игроке.
///
/// Держатель снаряда
/// Опциональная точка привязки. При передаче, снаряд будет привязан на нее
/// bool isPickedUp - получилось ли поднять снаряд
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();
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 = new Quaternion(0, 90f, 0, 0);
transform.localScale = baseLocalScale * heldScale;
if (fireScript != null) fireScript.BindProjectile(this);
StartCoroutine(IgnoreOwnerTemporarily(owner, ignoreOwnerTimeOnThrow));
CurrentState = State.Held;
launched = false;
hadBeenHeld = true;
return true;
}
///
/// Метод для получения точки привязки на игроке.
///
/// Игрок, который держит снаряд
/// Опциональная точка привязки. При передаче, снаряд будет привязан на нее
/// Transform attachPoint - точкка привязки
private Transform ResolveAttachPoint(GameObject holder, Transform preferred = null)
{
if (preferred) return preferred;
PlayerFireScript fireScript = holder.GetComponent();
if (fireScript && fireScript.attachPoint) return fireScript.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 launchDirection = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.right;
float finalSpeed = (speed > 0f) ? speed : initialSpeed;
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
rb.AddForce(launchDirection * finalSpeed * rb.mass, ForceMode2D.Impulse);
transform.up = launchDirection;
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);
}
///
/// Метод для просчета отскоков.
///
/// Коллизия, с которой столкнулся снаряд
/// bool isBounced - произошел ли отскок
private bool TryBounce(Collision2D collision)
{
if (Time.time - lastImpactTime < stickDelayAfterImpact)
return true;
Vector2 velocityIn = rb.linearVelocity;
float speed = velocityIn.magnitude;
if (speed < minBounceSpeed) return false;
Vector2 normal;
float impulseSum = 0f;
float minSeparation = float.PositiveInfinity;
if (collision.contactCount > 0)
{
Vector2 sum = Vector2.zero;
for (int i = 0; i < collision.contactCount; i++)
{
ContactPoint2D contactPoint = collision.GetContact(i);
float impulse = Mathf.Max(0.0001f, Mathf.Abs(contactPoint.normalImpulse));
sum += contactPoint.normal * impulse;
impulseSum += impulse;
if (contactPoint.separation < minSeparation) minSeparation = contactPoint.separation;
}
normal = (impulseSum > 0f ? sum / impulseSum : sum);
if (normal.sqrMagnitude < 1e-6f) normal = Vector2.up;
else normal.Normalize();
}
else
{
normal = Vector2.up;
minSeparation = 0f;
}
if (Vector2.Dot(velocityIn, normal) > 0f) normal = -normal;
float matBounciness = restitution;
float frictionA = 0f, frictionB = 0f;
var projectileColMat = col ? col.sharedMaterial : null;
if (projectileColMat)
{
matBounciness = Mathf.Max(matBounciness, projectileColMat.bounciness);
frictionA = Mathf.Clamp01(projectileColMat.friction);
}
var otherCol = collision.collider;
if (otherCol)
{
var otherMat = otherCol.sharedMaterial;
if (otherMat)
{
matBounciness = Mathf.Max(matBounciness, otherMat.bounciness);
frictionB = Mathf.Clamp01(otherMat.friction);
}
}
float tangentFactor = (1f - frictionA) * (1f - frictionB) * Mathf.Clamp01(tangentDamping);
Vector2 velocityNormal = Vector2.Dot(velocityIn, normal) * normal;
Vector2 velocityTangent = velocityIn - velocityNormal;
Vector2 velocityOut = (-velocityNormal) * matBounciness + velocityTangent * tangentFactor;
rb.linearVelocity = velocityOut;
float pushOut = 0.005f;
if (minSeparation < 0f) pushOut = Mathf.Max(pushOut, -minSeparation + 0.001f);
rb.position += normal * pushOut;
if (velocityOut.sqrMagnitude > 1e-6f)
transform.up = velocityOut.normalized;
if (otherCol.CompareTag("Ground") || otherCol.CompareTag("Trap")) 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;
}
///
/// Метод для поиска точки привязки на игроке.
///
/// Объект игрока
/// Transform attachPoint - точка привязки
private Transform FindAttachPoint(GameObject owner)
{
if (string.IsNullOrWhiteSpace(attachPointChildName)) return null;
var ownerTransform = owner.transform.Find(attachPointChildName);
return ownerTransform;
}
///
/// Метод для обработки возвращения снаряда к игроку.
/// При возвращении снаряд игнорирует любые коллизии.
///
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 viewPoint = mainCamera.WorldToViewportPoint(transform.position);
isVisible = viewPoint.x >= -margin && viewPoint.x <= 1f + margin
&& viewPoint.y >= -margin && viewPoint.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
}
}