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 } }