using System; using System.Collections; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.InputSystem; 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 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 Start() { cannonRadius = projectileObject.GetComponent()?.radius ?? projectileObject.GetComponent().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(); 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"); 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.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(); var catchSize = Random.Range((int)randomCatch.x, (int)randomCatch.y + 1); hitBoids.CatchBoid(hitColliders[i], catchSize); } } else { hit.transform.GetComponent()?.TakeDamage(power); } if (marker) { Destroy(marker); } } #endregion } }