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 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("Assets/05.Prefabs/Particles/GrenadeFire", "GrenadeFireOBJ", ".prefab"); visualLook = transform.Find("VisualLook"); launchTransform = transform.Find("LaunchPosition"); predictedLine = transform.Find("CannonLineRenderer").GetComponent(); if (predictedLine) { predictedLine.gameObject.SetActive(false); } hitMarker = Utils.LoadFromFolder("Assets/05.Prefabs", "HitMarker", ".prefab"); } private void InitStart() { CannonRadius = projectileObject.GetComponent()?.radius ?? projectileObject.GetComponent().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.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 } }