OldBlueWater/BlueWater/Assets/02.Scripts/Player/Cannon.cs

489 lines
19 KiB
C#

using System;
using System.Collections;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Serialization;
using Random = UnityEngine.Random;
// ReSharper disable once CheckNamespace
namespace BlueWaterProject
{
public class Cannon : MonoBehaviour
{
/***********************************************************************
* Definitions
***********************************************************************/
#region Definitions
private enum LaunchType
{
NONE = -1,
FIXED_ANGLE,
FIXED_SPEED
}
#endregion
/***********************************************************************
* Variables
***********************************************************************/
#region Variables
// 초기화 방식
[Title("초기화 방식")]
[SerializeField] private bool autoInit = true;
// 컴포넌트
[Title("컴포넌트")]
[SerializeField] private PlayerInput playerInput;
[SerializeField] private GameObject projectileObject;
[SerializeField] private Transform visualLook;
[SerializeField] private Transform launchTransform;
[SerializeField] private LineRenderer predictedLine;
[SerializeField] private GameObject hitMarker;
[SerializeField] private GameObject directionIndicator;
[SerializeField] private ProcessBar launchProcessBar;
[SerializeField] private Transform instantiateObjects;
// 게이지 옵션
[Title("게이지 옵션")]
[Range(0.1f, 5f), Tooltip("게이지가 모두 차는데 걸리는 시간\n게이지는 0 ~ 1의 값을 가짐")]
[SerializeField] private float gaugeChargingTime = 1f;
// 발사 옵션
[Title("발사 옵션")]
[Range(0f, 3f), Tooltip("발사 재사용 시간")]
[SerializeField] private float launchCooldown = 1f;
[Range(1f, 100f), Tooltip("발사될 거리 계수\nchargingGauge * 변수값")]
[SerializeField] private float distanceCoefficient = 40f;
[Tooltip("발사 방식")]
[SerializeField] private LaunchType launchType = LaunchType.FIXED_ANGLE;
[ShowIf("@launchType == LaunchType.FIXED_SPEED")]
[Range(0f, 100f), Tooltip("발사 속도")]
[SerializeField] private float launchSpeed = 20f;
[ShowIf("@launchType == LaunchType.FIXED_ANGLE")]
[Range(0f, 60f), Tooltip("발사 각도")]
[SerializeField] private float launchAngle = 10f;
[Title("발사 예측 옵션")]
[SerializeField] private bool isUsingPredictLine;
[ShowIf("@isUsingPredictLine")]
[Range(1, 200), Tooltip("발사 예측선 갯수")]
[SerializeField] private int lineMaxPoint = 100;
[ShowIf("@isUsingPredictLine")]
[Range(0.001f, 1f), Tooltip("발사 예측선 간격")]
[SerializeField] private float lineInterval = 0.025f;
// 기타 옵션
[Title("기타 옵션")]
[Tooltip("랜덤으로 잡힐 물고기 마릿수")]
[SerializeField] private Vector2 randomCatch = new(1, 4);
[SerializeField] private float mouseRayDistance = 500f;
[SerializeField] private float rayDistance = 10f;
[SerializeField] private LayerMask hitLayer;
[SerializeField] private LayerMask waterLayer;
[SerializeField] private LayerMask boidsLayer;
// 카메라 효과 옵션
[Title("카메라 효과 옵션")]
[SerializeField] private float cameraShakePower = 2f;
[SerializeField] private float cameraShakeDuration = 0.3f;
// 실시간 데이터
[Title("실시간 데이터")]
[DisableIf("@true")]
[SerializeField] private bool isLaunchMode;
[DisableIf("@true")]
[SerializeField] private bool isCharging;
[DisableIf("@true")]
[SerializeField] private bool isReloading;
[DisableIf("@true")]
[SerializeField] private float chargingGauge;
[DisableIf("@true")]
[SerializeField] private float previousGauge;
private float cannonRadius;
private Vector3 launchVelocity;
private Collider[] hitColliders;
private GameObject newHitMarker;
private const int MAX_HIT_SIZE = 8;
#endregion
/***********************************************************************
* Unity Events
***********************************************************************/
#region Unity Events
private void Awake()
{
if (autoInit)
{
Init();
}
}
private void Start()
{
cannonRadius = projectileObject.GetComponent<SphereCollider>()?.radius ??
projectileObject.GetComponent<ParticleWeapon>().colliderRadius;
launchProcessBar = UiManager.Inst.OceanUi.ProcessBar;
hitColliders = new Collider[MAX_HIT_SIZE];
}
private void OnEnable()
{
playerInput.actions.FindAction("ToggleCannon").started += _ => ToggleCannon();
playerInput.actions.FindAction("FireCannon").started += _ => ChargeCannon();
playerInput.actions.FindAction("FireCannon").canceled += _ => FireCannon();
}
private void OnDisable()
{
playerInput.actions.FindAction("ToggleCannon").started += _ => ToggleCannon();
playerInput.actions.FindAction("FireCannon").started -= _ => ChargeCannon();
playerInput.actions.FindAction("FireCannon").canceled -= _ => FireCannon();
}
private void Update()
{
HandleFireCannon();
}
#endregion
/***********************************************************************
* Init Methods
***********************************************************************/
#region Init Methods
[Button("셋팅 초기화")]
private void Init()
{
playerInput = GetComponentInParent<PlayerInput>();
projectileObject = Utils.LoadFromFolder<GameObject>("Assets/05.Prefabs/Particles/GrenadeFire", "GrenadeFireOBJ", ".prefab");
visualLook = transform.Find("VisualLook");
launchTransform = transform.Find("LaunchPosition");
predictedLine = transform.Find("CannonLineRenderer").GetComponent<LineRenderer>();
if (predictedLine)
{
predictedLine.gameObject.SetActive(false);
}
hitMarker = Utils.LoadFromFolder<GameObject>("Assets/05.Prefabs", "HitMarker", ".prefab");
directionIndicator = transform.parent.Find("DirectionIndicator")?.gameObject;
if (directionIndicator)
{
directionIndicator.SetActive(false);
}
instantiateObjects = GameObject.Find("InstantiateObjects").transform;
waterLayer = LayerMask.GetMask("Water");
boidsLayer = LayerMask.GetMask("Boids");
}
#endregion
/***********************************************************************
* PlayerInput
***********************************************************************/
#region PlayerInput
private void ToggleCannon()
{
isLaunchMode = !isLaunchMode;
if (directionIndicator)
{
directionIndicator.SetActive(isLaunchMode);
}
launchProcessBar.SetActive(isLaunchMode);
if (!isLaunchMode)
{
isCharging = false;
chargingGauge = 0f;
previousGauge = chargingGauge;
launchProcessBar.SetFillAmount(0f);
launchProcessBar.SetRotateZ(previousGauge * -360f);
launchProcessBar.SetRotateZ(0f);
launchProcessBar.SetSliderValue(0f);
}
}
private void ChargeCannon()
{
if (!isLaunchMode) return;
if (isReloading)
{
StartCoroutine(UiManager.Inst.OceanUi.ProcessBar.ShakeProcessBarCoroutine());
}
else
{
predictedLine.gameObject.SetActive(true);
if (hitMarker)
{
newHitMarker = Instantiate(hitMarker, Vector3.zero, hitMarker.transform.rotation, instantiateObjects);
newHitMarker.transform.localScale *= cannonRadius * 2f;
hitMarker.SetActive(true);
}
isCharging = true;
chargingGauge = 0f;
}
}
private void FireCannon()
{
if (!isLaunchMode || !isCharging) return;
isCharging = false;
predictedLine.gameObject.SetActive(false);
previousGauge = chargingGauge;
chargingGauge = 0f;
launchProcessBar.SetFillAmount(0f);
launchProcessBar.SetRotateZ(previousGauge * -360f);
Launch();
StartCoroutine(LaunchCoolDown(launchCooldown));
}
#endregion
/***********************************************************************
* Methods
***********************************************************************/
#region Methods
private void HandleFireCannon()
{
if (!isLaunchMode) return;
var ray = CameraManager.Inst.MainCam.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out var hit, mouseRayDistance, waterLayer, QueryTriggerInteraction.Collide))
{
var directionToMouse = (hit.point - transform.position).normalized;
directionToMouse.y = 0f;
var lookRotation = Quaternion.LookRotation(directionToMouse);
if (directionIndicator)
{
var indicatorRotationDirection = Quaternion.Euler(0f, lookRotation.eulerAngles.y, 0f);
directionIndicator.transform.rotation = indicatorRotationDirection;
}
var cannonRotationDirection = Quaternion.Euler(transform.rotation.eulerAngles.x, lookRotation.eulerAngles.y, 0f);
transform.rotation = cannonRotationDirection;
}
if (!isCharging) return;
if (chargingGauge < 1f)
{
if (gaugeChargingTime == 0f)
{
gaugeChargingTime = 1f;
}
chargingGauge += 1 / gaugeChargingTime * Time.deltaTime;
chargingGauge = Mathf.Clamp(chargingGauge, 0f, 1f);
}
else
{
chargingGauge = 1f;
}
launchProcessBar.SetFillAmount(chargingGauge);
CalculateLaunchTrajectory();
}
private void CalculateLaunchTrajectory()
{
var startPosition = launchTransform.position;
var endPosition = CalculateEndPosition();
switch (launchType)
{
case LaunchType.NONE:
break;
case LaunchType.FIXED_ANGLE:
var currentEulerX = visualLook.eulerAngles.x - 360;
launchTransform.localRotation = Quaternion.Euler(currentEulerX + launchAngle, 0, 0);
var d = Vector3.Distance(new Vector3(endPosition.x, 0, endPosition.z), new Vector3(startPosition.x, 0, startPosition.z));
var h = endPosition.y - startPosition.y;
var theta = launchAngle * Mathf.Deg2Rad;
var g = Physics.gravity.magnitude;
var v0 = Mathf.Sqrt((g * d * d) / (2 * Mathf.Cos(theta) * Mathf.Cos(theta) * (d * Mathf.Tan(theta) - h)));
launchVelocity = CalculateVelocityFromAngleAndSpeed(startPosition, theta, v0);
break;
case LaunchType.FIXED_SPEED:
var launchPosition = launchTransform.position;
var x = Vector3.Distance(new Vector3(endPosition.x, 0, endPosition.z), new Vector3(launchPosition.x, 0, launchPosition.z));
var y = endPosition.y - launchPosition.y;
var angle = CalculateAngleForFixedSpeed(x, y, launchSpeed);
launchTransform.localRotation = Quaternion.Euler(-angle, 0, 0);
launchVelocity = launchTransform.forward * launchSpeed;
break;
default:
throw new ArgumentOutOfRangeException();
}
PredictLine(startPosition);
}
private float CalculateAngleForFixedSpeed(float x, float y, float speed)
{
var g = Physics.gravity.magnitude;
var speedSq = speed * speed;
var underRoot = speedSq * speedSq - g * (g * x * x + 2 * y * speedSq);
if (underRoot < 0)
{
Debug.LogError("Unreachable target with given speed.");
return 0;
}
var root = Mathf.Sqrt(underRoot);
var angle1 = Mathf.Atan((speedSq + root) / (g * x));
var angle2 = Mathf.Atan((speedSq - root) / (g * x));
var selectedAngle = Mathf.Min(angle1, angle2) * Mathf.Rad2Deg;
return selectedAngle;
}
private Vector3 CalculateEndPosition()
{
var endPosition = launchTransform.position + transform.forward * (chargingGauge * distanceCoefficient);
Debug.DrawRay(endPosition, Vector3.down * rayDistance, Color.blue, 3f);
if (Physics.Raycast(endPosition, Vector3.down, out var hit, rayDistance, hitLayer, QueryTriggerInteraction.Collide))
{
Debug.DrawRay(hit.point, Vector3.down * rayDistance, Color.red, 3f);
return hit.point;
}
print("?");
return endPosition;
}
private Vector3 CalculateVelocityFromAngleAndSpeed(Vector3 startPosition, float angleRad, float speed)
{
var direction = launchTransform.forward;
direction.y = 0;
direction.Normalize();
var vx = speed * Mathf.Cos(angleRad);
var vy = speed * Mathf.Sin(angleRad);
var velocity = new Vector3(direction.x * vx, vy, direction.z * vx);
return velocity;
}
private void PredictLine(Vector3 startPosition)
{
if (!isUsingPredictLine) return;
UpdateLineRender(lineMaxPoint, (0, launchTransform.position));
var currentVelocity = launchVelocity;
var predictPosition = startPosition;
for (var i = 0; i < lineMaxPoint; i++)
{
currentVelocity = GetNextPredictedPosition(currentVelocity, 0f, lineInterval);
var nextPosition = predictPosition + currentVelocity * lineInterval;
predictPosition = nextPosition;
UpdateLineRender(lineMaxPoint, (i, predictPosition));
if (newHitMarker)
{
if (Physics.Raycast(predictPosition, Vector3.down, out var hit, rayDistance, hitLayer, QueryTriggerInteraction.Collide))
{
newHitMarker.transform.position = hit.point;
var hitRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
newHitMarker.transform.rotation = Quaternion.Euler(90, 0, 0) * hitRotation;
}
}
}
}
private Vector3 GetNextPredictedPosition(Vector3 currentVelocity, float drag, float increment)
{
currentVelocity += Physics.gravity * increment;
currentVelocity *= Mathf.Clamp01(1f - drag * increment);
return currentVelocity;
}
private void UpdateLineRender(int count, (int point, Vector3 pos) pointPos)
{
predictedLine.positionCount = count;
predictedLine.SetPosition(pointPos.point, pointPos.pos);
}
private IEnumerator LaunchCoolDown(float waitTime)
{
var time = 0f;
launchProcessBar.SetSliderValue(0f);
launchProcessBar.SetActiveReloadSlider(true);
while (time <= waitTime)
{
time += Time.deltaTime;
var sliderValue = time > 0 ? time / waitTime : 0f;
launchProcessBar.SetSliderValue(sliderValue);
yield return null;
}
isReloading = false;
launchProcessBar.SetActiveReloadSlider(false);
}
private void Launch()
{
VisualFeedbackManager.Inst.CameraShake(CameraManager.Inst.OceanCamera.BaseShipCam, cameraShakePower, cameraShakeDuration);
var projectile = Instantiate(projectileObject, launchTransform.position, Quaternion.identity);
var particleWeapon = projectile.GetComponent<ParticleWeapon>();
particleWeapon.SetHitMarker(newHitMarker);
particleWeapon.onHitAction.AddListener(HitAction);
particleWeapon.Rb.AddForce(launchVelocity, ForceMode.VelocityChange);
isReloading = true;
}
private void HitAction(RaycastHit hit, float power, GameObject marker = null)
{
if (hit.collider.gameObject.layer == LayerMask.NameToLayer("Water"))
{
var maxSize = Physics.OverlapSphereNonAlloc(hit.point, cannonRadius, hitColliders, boidsLayer,
QueryTriggerInteraction.Collide);
for (var i = 0; i < maxSize; i++)
{
var hitBoids = hitColliders[i].GetComponentInParent<Boids>();
var catchSize = Random.Range((int)randomCatch.x, (int)randomCatch.y + 1);
hitBoids.CatchBoid(hitColliders[i], catchSize);
}
}
else
{
hit.transform.GetComponent<IDamageable>()?.TakeDamage(power);
}
if (marker)
{
Destroy(marker);
}
}
#endregion
}
}