500 lines
16 KiB
C#
500 lines
16 KiB
C#
using UnityEngine;
|
|
using RR.Common.Interfaces;
|
|
using RR.Game2D.Core;
|
|
using RR.Game2D.Behaviour;
|
|
using RR.Game2D.Core.Interfaces;
|
|
|
|
namespace RR.Game2D.Entities.Projectiles
|
|
{
|
|
[DisallowMultipleComponent]
|
|
[RequireComponent(typeof(Rigidbody2D),
|
|
typeof(CapsuleCollider2D))]
|
|
public class PickableProjectile : PhysicsProjectile, IPickable, IBouncy, IRecallable
|
|
{
|
|
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;
|
|
|
|
[Header("Приземление")]
|
|
[SerializeField] private float groundVelocity = 1f;
|
|
[SerializeField] private float maxGroundSlideTime = 1f;
|
|
[SerializeField] private bool lockWhenLanded = true;
|
|
|
|
[Header("Отскоки")]
|
|
[SerializeField] private int maxBounces = 3;
|
|
[SerializeField] private float minBounceSpeed = 0.8f;
|
|
[SerializeField] private float stickDelayAfterImpact = 0.03f;
|
|
|
|
[Header("Возврат")]
|
|
[SerializeField, Range(0, 100)] int recallScreenMarginPercent = 5;
|
|
[SerializeField] private float recallArriveDistance = 1f;
|
|
[SerializeField] private float recallSpeed = 16f;
|
|
[SerializeField] private float recallStartDistance = 8f;
|
|
|
|
[Header("Gizmos")]
|
|
[SerializeField] private bool drawGizmos;
|
|
[SerializeField] private Color gizmoColor = new (0.2f, 0.8f, 1f, 0.35f);
|
|
|
|
private Transform holderAttachPoint;
|
|
private int bounceCounter;
|
|
private float lastImpactTime = -999f;
|
|
private readonly Vector3 baseLocalScale = Vector3.one;
|
|
|
|
private bool hadBeenHeld;
|
|
private GameObject lastKnownHolder;
|
|
private float lastGroundTouchTime;
|
|
|
|
private GameObject jumpPad;
|
|
|
|
// -------------------------------------------
|
|
// Методы Unity
|
|
//
|
|
|
|
#region Unity Methods
|
|
|
|
//---------------------------
|
|
// Singleton
|
|
//---------------------------
|
|
protected override void Awake()
|
|
{
|
|
rb = GetComponent<Rigidbody2D>();
|
|
|
|
col = GetComponent<CapsuleCollider2D>();
|
|
col.isTrigger = true;
|
|
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
|
|
Instance = this;
|
|
|
|
jumpPad = new GameObject("Projectile Jump Pad");
|
|
jumpPad.AddComponent<BoxCollider2D>();
|
|
jumpPad.AddComponent<JumpPad2D>();
|
|
jumpPad.GetComponent<BoxCollider2D>().isTrigger = true;
|
|
Vector3 jumpPadScale = jumpPad.transform.localScale;
|
|
jumpPadScale.y = 0.1f;
|
|
jumpPad.transform.localScale = jumpPadScale;
|
|
jumpPad.SetActive(false);
|
|
jumpPad.GetComponent<JumpPad2D>().PlayerJumped += OnPlayerJumped;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (Instance == this) Instance = null;
|
|
Destroy(jumpPad);
|
|
}
|
|
//---------------------------
|
|
//
|
|
//---------------------------
|
|
|
|
private void Start()
|
|
{
|
|
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
|
col.isTrigger = false;
|
|
|
|
if (CurrentState != State.Held)
|
|
BecomeGrounded();
|
|
}
|
|
|
|
protected override void FixedUpdate()
|
|
{
|
|
base.FixedUpdate();
|
|
|
|
if (!lockWhenLanded && CurrentState == State.Airborne)
|
|
{
|
|
if (rb.linearVelocity.sqrMagnitude < (groundVelocity * groundVelocity))
|
|
BecomeGrounded();
|
|
}
|
|
|
|
if ((CurrentState == State.Airborne || CurrentState == State.Returning) && rb.linearVelocity.sqrMagnitude > 0.0001f)
|
|
transform.up = rb.linearVelocity.normalized;
|
|
|
|
if (CurrentState == State.Airborne && rb.linearVelocity.magnitude < groundVelocity)
|
|
BecomeGrounded();
|
|
|
|
if (hadBeenHeld && CurrentState != State.Held)
|
|
Recall();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (!IsInHitLayer(collision.collider.gameObject.layer)) return;
|
|
Vector2 point = collision.GetContact(0).point;
|
|
OnHit(collision, point, collision.relativeVelocity);
|
|
|
|
if (CurrentState == State.Airborne)
|
|
{
|
|
if (TryBounce(collision))
|
|
{
|
|
lastImpactTime = Time.time;
|
|
return;
|
|
}
|
|
|
|
BecomeGrounded();
|
|
}
|
|
|
|
if (collision.collider.CompareTag("Ground"))
|
|
lastGroundTouchTime = Time.time;
|
|
}
|
|
|
|
private void OnCollisionStay2D(Collision2D collision)
|
|
{
|
|
if (collision.collider.CompareTag("Ground"))
|
|
{
|
|
if (Time.time - lastGroundTouchTime > maxGroundSlideTime)
|
|
BecomeGrounded();
|
|
}
|
|
}
|
|
|
|
|
|
protected override void OnTriggerEnter2D(Collider2D other)
|
|
{
|
|
if (other.CompareTag("Player"))
|
|
{
|
|
if (TryPickup(other.gameObject)) return;
|
|
}
|
|
else if (other.CompareTag("Enemy"))
|
|
{
|
|
other.TryGetComponent<EnemyController>(out var controller);
|
|
controller.Die();
|
|
}
|
|
else
|
|
{
|
|
base.OnTriggerEnter2D(other);
|
|
}
|
|
}
|
|
//
|
|
//
|
|
// -------------------------------------------
|
|
|
|
#endregion
|
|
|
|
// -------------------------------------------
|
|
// Переопределенные методы родителей
|
|
//
|
|
|
|
#region Parent Methods
|
|
|
|
protected override void OnHit(Collision2D collision, Vector2 point, Vector2 relativeVelocity)
|
|
{
|
|
//TryDealDamage(collision.collider, point, relativeVelocity);
|
|
PlayFX(point);
|
|
|
|
OnImpactFinal(collision);
|
|
}
|
|
|
|
protected override void OnImpactFinal(Collision2D collision)
|
|
{
|
|
if (!TryBounce(collision))
|
|
BecomeGrounded();
|
|
}
|
|
|
|
protected override void OnImpactFinal(Collider2D other)
|
|
{
|
|
if (Time.time - lastImpactTime > stickDelayAfterImpact)
|
|
BecomeGrounded();
|
|
}
|
|
|
|
#endregion
|
|
|
|
//
|
|
//
|
|
// -------------------------------------------
|
|
|
|
// -------------------------------------------
|
|
// Подбор
|
|
//
|
|
|
|
#region Pickup
|
|
|
|
public bool TryPickup(GameObject newOwner, Transform optionalAttachPoint = null)
|
|
{
|
|
if (CurrentState == State.Held) return true;
|
|
|
|
owner = newOwner;
|
|
lastKnownHolder = newOwner;
|
|
|
|
holderAttachPoint = optionalAttachPoint ?? FindAttachPoint(newOwner);
|
|
var fireScript = newOwner.GetComponent<PlayerFireScript>();
|
|
if (!holderAttachPoint && fireScript != null)
|
|
holderAttachPoint = fireScript.attachPoint;
|
|
if (!holderAttachPoint)
|
|
holderAttachPoint = newOwner.transform;
|
|
|
|
rb.linearVelocity = Vector2.zero;
|
|
rb.angularVelocity = 0f;
|
|
rb.bodyType = RigidbodyType2D.Kinematic;
|
|
col.enabled = false;
|
|
|
|
transform.SetParent(holderAttachPoint);
|
|
transform.SetPositionAndRotation(holderAttachPoint.TransformPoint(heldLocalOffset), 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;
|
|
|
|
jumpPad.SetActive(false);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Метод для получения точки привязки на игроке.
|
|
/// </summary>
|
|
/// <param name="holder">Игрок, который держит снаряд</param>
|
|
/// <param name="preferred">Опциональная точка привязки. При передаче, снаряд будет привязан на нее</param>
|
|
/// <returns>Transform attachPoint - точка привязки</returns>
|
|
private Transform FindAttachPoint(GameObject holder, Transform preferred = null)
|
|
{
|
|
if (preferred) return preferred;
|
|
|
|
PlayerFireScript fireScript = holder.GetComponent<PlayerFireScript>();
|
|
if (fireScript && fireScript.attachPoint) return fireScript.attachPoint;
|
|
|
|
return holder.transform;
|
|
}
|
|
|
|
#endregion
|
|
|
|
//
|
|
//
|
|
// -------------------------------------------
|
|
|
|
// -------------------------------------------
|
|
// Отскок
|
|
//
|
|
|
|
#region Bounce
|
|
|
|
public bool TryBounce(Collision2D collision)
|
|
{
|
|
if (Time.time - lastImpactTime < stickDelayAfterImpact) return true;
|
|
|
|
if (bounceCounter > maxBounces) return false;
|
|
|
|
Vector2 velocityIn = rb.linearVelocity;
|
|
float speed = velocityIn.magnitude;
|
|
|
|
if (speed < minBounceSpeed) return false;
|
|
|
|
Vector2 direction = Vector2.Reflect(velocityIn, collision.contacts[0].normal);
|
|
|
|
rb.linearVelocity = col.sharedMaterial.bounciness * direction;
|
|
|
|
|
|
if (collision.collider.CompareTag("Ground") || collision.collider.CompareTag("Trap"))
|
|
bounceCounter++;
|
|
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
//
|
|
//
|
|
// -------------------------------------------
|
|
|
|
|
|
// -------------------------------------------
|
|
// Запуск
|
|
//
|
|
|
|
#region Launch
|
|
|
|
public override void Launch(Vector2 direction, float speed = -1, GameObject thrower = null)
|
|
{
|
|
if (CurrentState == State.Held)
|
|
transform.SetParent(null);
|
|
|
|
owner = thrower;
|
|
lastKnownHolder = thrower;
|
|
holderAttachPoint = null;
|
|
|
|
rb.bodyType = RigidbodyType2D.Dynamic;
|
|
col.enabled = true;
|
|
col.isTrigger = false;
|
|
|
|
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(finalSpeed * rb.mass * launchDirection, ForceMode2D.Impulse);
|
|
|
|
transform.up = launchDirection;
|
|
transform.localScale = baseLocalScale;
|
|
|
|
if (thrower != null && ignoreOwnerTimeOnThrow > 0f)
|
|
StartCoroutine(IgnoreOwnerTemporarily(thrower, ignoreOwnerTimeOnThrow));
|
|
|
|
CurrentState = State.Airborne;
|
|
bounceCounter = 0;
|
|
}
|
|
|
|
#endregion
|
|
|
|
//
|
|
//
|
|
// -------------------------------------------
|
|
|
|
// -------------------------------------------
|
|
// Возвращение
|
|
//
|
|
|
|
#region Recall
|
|
|
|
public void Recall()
|
|
{
|
|
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 = FindAttachPoint(playerGo);
|
|
|
|
bool isVisible;
|
|
var mainCamera = Camera.main;
|
|
float margin = recallScreenMarginPercent / 100f;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
Vector2 dir = (dist > 0.001f) ? (toOwner / dist) : rb.linearVelocity.normalized;
|
|
rb.linearVelocity = dir * recallSpeed;
|
|
|
|
if (rb.linearVelocity.sqrMagnitude > 0.01f)
|
|
transform.up = rb.linearVelocity.normalized;
|
|
|
|
if (dist <= recallArriveDistance)
|
|
TryPickup(playerGo);
|
|
}
|
|
|
|
#endregion
|
|
|
|
//
|
|
//
|
|
// -------------------------------------------
|
|
|
|
// -------------------------------------------
|
|
// Собственные незаисимые методы
|
|
//
|
|
|
|
#region Self methods
|
|
|
|
/// <summary>
|
|
/// Метод для заморозки снаряда на земле.
|
|
/// </summary>
|
|
private void BecomeGrounded()
|
|
{
|
|
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;
|
|
col.isTrigger = true;
|
|
|
|
transform.SetParent(null, true);
|
|
bounceCounter = 0;
|
|
|
|
MoveJumpPad();
|
|
jumpPad.SetActive(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Метод для перемещения прыжковой платформы Тамагочика
|
|
/// </summary>
|
|
private void MoveJumpPad()
|
|
{
|
|
float sizeX = col.bounds.size.y;
|
|
float sizeY = col.bounds.size.x;
|
|
|
|
Vector3 jumpPadPos = transform.position;
|
|
|
|
float jumpPadY = Mathf.Sqrt(sizeX * sizeX * Mathf.Pow(Mathf.Cos(transform.localEulerAngles.z), 2) + sizeY * sizeY * Mathf.Pow(Mathf.Sin(transform.localEulerAngles.z), 2));
|
|
|
|
jumpPadPos.y = jumpPadPos.y + 0.5f * jumpPadY;
|
|
jumpPad.transform.position = jumpPadPos;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Метод для обработки событий при отпрыгивания игрока от прыжковой платформы.
|
|
/// </summary>
|
|
/// <param name="playerObj"></param>
|
|
private void OnPlayerJumped(GameObject playerObj)
|
|
{
|
|
TryPickup(playerObj);
|
|
}
|
|
|
|
#endregion
|
|
|
|
//
|
|
//
|
|
// -------------------------------------------
|
|
}
|
|
} |