OldBlueWater/BlueWater/Assets/02.Scripts/Player/Cannon.cs
NTG 86f9d2607e Closes #213 #214 해적선 Ai 추가 및 항해 씬 취합
+ 해적선 Ai 추가
  ㄴ 패트롤, 추격, 공격 등의 패턴 적용
+ Cannon 클래스 분리
  ㄴ 캐논 자체의 기능만 남기고, Player는 CannonController와 연결해서 사용
+ Player, Pirate 용 cannon projectile 분리
+ New input system 네이밍 변경
  ㄴ ToggleCannon -> ToggleLaunchMode
  ㄴ FireCannon -> LaunchCannon
+ 해적선 Ai에 Rayfire(파괴) 기능 테스트용 추가
2024-03-05 12:47:17 +09:00

340 lines
13 KiB
C#

using System;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Events;
// 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
// 컴포넌트
[TitleGroup("컴포넌트"), BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)]
[Required, SerializeField] private GameObject projectileObject;
[BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)]
[Required, SerializeField] private Transform visualLook;
[BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)]
[Required, SerializeField] private Transform launchTransform;
[BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)]
[SerializeField] private LineRenderer predictedLine;
[BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)]
[SerializeField] private GameObject hitMarker;
[BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)]
[Required, SerializeField] private Transform instantiateObjects;
// 대포 기본 설정
[field: TitleGroup("대포 기본 설정")]
// 발사 기능
[field: BoxGroup("대포 기본 설정/발사 기능")]
[field: Range(0f, 10f), Tooltip("발사 재사용 시간")]
[field: SerializeField] public float LaunchCooldown { get; set; } = 1f;
[BoxGroup("대포 기본 설정/발사 기능")]
[Tooltip("대포 공격력")]
[SerializeField] private float damage = 20f;
[BoxGroup("대포 기본 설정/발사 기능")]
[Range(1f, 100f), Tooltip("발사될 거리 계수\nchargingGauge * 변수값")]
[SerializeField] private float distanceCoefficient = 40f;
[BoxGroup("대포 기본 설정/발사 기능")]
[Tooltip("발사 방식")]
[SerializeField] private LaunchType launchType = LaunchType.FIXED_ANGLE;
[BoxGroup("대포 기본 설정/발사 기능")]
[ShowIf("@launchType == LaunchType.FIXED_SPEED"), Range(0f, 100f), Tooltip("발사 속도")]
[SerializeField] private float launchSpeed = 20f;
[BoxGroup("대포 기본 설정/발사 기능")]
[ShowIf("@launchType == LaunchType.FIXED_ANGLE"), Range(0f, 60f), Tooltip("발사 각도")]
[SerializeField] private float launchAngle = 10f;
// 예측 기능
[BoxGroup("대포 기본 설정/예측 기능")]
[SerializeField] private bool isUsingPredictLine;
[BoxGroup("대포 기본 설정/예측 기능")]
[ShowIf("@isUsingPredictLine"), Range(1, 300), Tooltip("발사 예측선 갯수")]
[SerializeField] private int lineMaxPoint = 200;
[BoxGroup("대포 기본 설정/예측 기능")]
[ShowIf("@isUsingPredictLine"), Range(0.001f, 1f), Tooltip("발사 예측선 간격")]
[SerializeField] private float lineInterval = 0.025f;
// 기타
[BoxGroup("대포 기본 설정/기타")]
[SerializeField] private float rayDistance = 100f;
[BoxGroup("대포 기본 설정/기타")]
[SerializeField] private LayerMask hitLayer;
// 실시간 상태
[field: TitleGroup("실시간 상태")]
[field: DisableIf("@true")]
[field: SerializeField] public bool IsReloading { get; set; }
public UnityEvent<RaycastHit, float, GameObject> onHitAction;
public float CannonRadius { get; private set; }
private Vector3 launchVelocity;
private GameObject newHitMarker;
private RaycastHit endPositionHit;
#endregion
/***********************************************************************
* Unity Events
***********************************************************************/
#region Unity Events
private void Start()
{
InitStart();
}
#endregion
/***********************************************************************
* Init Methods
***********************************************************************/
#region Init Methods
[Button("셋팅 초기화")]
private void Init()
{
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");
}
private void InitStart()
{
CannonRadius = projectileObject.GetComponent<SphereCollider>()?.radius ??
projectileObject.GetComponent<ParticleWeapon>().colliderRadius;
}
#endregion
/***********************************************************************
* Methods
***********************************************************************/
#region Methods
public void CalculateLaunchTrajectory(Vector3 endPosition, bool isPredict = false)
{
var startPosition = launchTransform.position;
var d = Vector3.Distance(new Vector3(endPosition.x, 0, endPosition.z), new Vector3(startPosition.x, 0, startPosition.z));
var h = endPosition.y - startPosition.y;
switch (launchType)
{
case LaunchType.NONE:
break;
case LaunchType.FIXED_ANGLE:
var currentEulerX = Utils.NormalizeEulerAngle(visualLook.eulerAngles.x);
launchTransform.localRotation = Quaternion.Euler(currentEulerX + launchAngle, 0, 0);
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(theta, v0);
break;
case LaunchType.FIXED_SPEED:
var targetDirection = (endPosition - startPosition).normalized;
targetDirection.y = 0;
var angle = CalculateAngleForFixedSpeed(d, h, launchSpeed);
var launchDirection = Quaternion.LookRotation(targetDirection) * Quaternion.Euler(-angle, 0, 0);
launchTransform.rotation = launchDirection;
launchVelocity = launchTransform.forward * launchSpeed;
// var angle = CalculateAngleForFixedSpeed(d, h, launchSpeed);
//
// launchTransform.localRotation = Quaternion.Euler(-angle, 0, 0);
// launchVelocity = launchTransform.forward * launchSpeed;
break;
default:
throw new ArgumentOutOfRangeException();
}
if (isPredict)
{
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;
}
public Vector3 CalculateEndPosition(float chargingGauge)
{
var direction = transform.forward;
direction.y = 0f;
var startPosition = launchTransform.position + direction.normalized * (chargingGauge * distanceCoefficient);
Debug.DrawRay(startPosition, Vector3.down * rayDistance, Color.blue, 3f);
if (Physics.Raycast(startPosition, Vector3.down, out endPositionHit, rayDistance, hitLayer, QueryTriggerInteraction.Collide))
{
Debug.DrawRay(endPositionHit.point, Vector3.down * rayDistance, Color.red, 3f);
return endPositionHit.point;
}
print("?");
return startPosition;
}
private Vector3 CalculateVelocityFromAngleAndSpeed(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 (Physics.Raycast(predictPosition - currentVelocity.normalized * lineInterval, currentVelocity.normalized, out var hit, CannonRadius * 2, hitLayer, QueryTriggerInteraction.Collide))
{
UpdateLineRender(i + 1, (i, predictPosition));
if (newHitMarker)
{
newHitMarker.transform.position = predictPosition;
var hitRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
newHitMarker.transform.rotation = Quaternion.Euler(90, 0, 0) * hitRotation;
}
return;
}
}
}
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);
}
public void Launch()
{
var projectile = Instantiate(projectileObject, launchTransform.position, Quaternion.identity);
var particleWeapon = projectile.GetComponent<ParticleWeapon>();
particleWeapon.SetHitMarker(newHitMarker);
particleWeapon.onHitAction.AddListener((hit, _, marker) => onHitAction?.Invoke(hit, damage, marker));
particleWeapon.Rb.AddForce(launchVelocity, ForceMode.VelocityChange);
IsReloading = true;
}
public void LaunchAtTarget(Collider target)
{
CalculateLaunchTrajectory(target.bounds.center, true);
StartChargeCannon();
Launch();
StartCoroutine(Utils.CoolDown(LaunchCooldown, () => IsReloading = false));
}
public void ExitLaunchMode()
{
SetActivePredictLine(false);
if (newHitMarker)
{
Destroy(newHitMarker);
}
}
public void StartChargeCannon()
{
SetActivePredictLine(true);
if (hitMarker)
{
newHitMarker = Instantiate(hitMarker, Vector3.zero, hitMarker.transform.rotation, instantiateObjects);
newHitMarker.transform.localScale *= CannonRadius * 2f;
hitMarker.SetActive(true);
}
}
public void SetActivePredictLine(bool value)
{
if (isUsingPredictLine)
{
predictedLine.gameObject.SetActive(value);
}
}
#endregion
}
}