Danro/Assets/Resources/Scripts/Projectiles/PickableProjectile.cs
2025-09-18 17:05:15 +03:00

434 lines
16 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 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);
}
}
/// <summary>
/// Метод для подбора снаряда игроком.
/// Привязывает снаряд к точке привязки на игроке.
/// </summary>
/// <param name="newHolder">Держатель снаряда</param>
/// <param name="optionalAttachPoint">Опциональная точка привязки. При передаче, снаряд будет привязан на нее</param>
/// <returns>bool isPickedUp - получилось ли поднять снаряд</returns>
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 = 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;
}
/// <summary>
/// Метод для получения точки привязки на игроке.
/// </summary>
/// <param name="holder">Игрок, который держит снаряд</param>
/// <param name="preferred">Опциональная точка привязки. При передаче, снаряд будет привязан на нее</param>
/// <returns>Transform attachPoint - точкка привязки</returns>
private Transform ResolveAttachPoint(GameObject holder, Transform preferred = null)
{
if (preferred) return preferred;
PlayerFireScript fireScript = holder.GetComponent<PlayerFireScript>();
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);
}
/// <summary>
/// Метод для просчета отскоков.
/// </summary>
/// <param name="collision">Коллизия, с которой столкнулся снаряд</param>
/// <returns>bool isBounced - произошел ли отскок</returns>
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;
}
/// <summary>
/// Метод для заморозки снаряда на земле.
/// </summary>
/// <param name="collision">Коллизия, с которой столкнулся снаряд</param>
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;
}
/// <summary>
/// Метод для поиска точки привязки на игроке.
/// </summary>
/// <param name="owner">Объект игрока</param>
/// <returns>Transform attachPoint - точка привязки</returns>
private Transform FindAttachPoint(GameObject owner)
{
if (string.IsNullOrWhiteSpace(attachPointChildName)) return null;
var ownerTransform = owner.transform.Find(attachPointChildName);
return ownerTransform;
}
/// <summary>
/// Метод для обработки возвращения снаряда к игроку.
/// При возвращении снаряд игнорирует любые коллизии.
/// </summary>
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
}
}