using System; using System.Collections; using System.Collections.Generic; using NWH.DWP2.WaterData; using NWH.DWP2.WaterObjects; using Sirenix.OdinInspector; using UnityEngine; using Random = UnityEngine.Random; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public enum EscapeMode { NONE = -1, STRAIGHT, ZIGZAG, TOWARDS } public class Boids : MonoBehaviour { // 군집(떼) 설정 [Title("군집(떼) 설정")] [Tooltip("Boid 프리팹")] [SerializeField] private Boid boidPrefab; [Range(1, 1000)] [Tooltip("생성할 개체 수")] [SerializeField] private int boidCount = 5; [field: Tooltip("개체의 랜덤 속도 값\nx == Min\ny == Max")] [field: SerializeField] public Vector2 RandomSpeedRange { get; private set; } = new(5f, 10f); [Range(5, 100)] [Tooltip("개체 생성 범위")] [SerializeField] private float spawnRange = 10; [Tooltip("자동 재생성 기능 여부")] [SerializeField] private bool isAutoRespawn = true; [Tooltip("자동 재생성하는데 걸리는 시간")] [ShowIf("@isAutoRespawn")] [SerializeField] private Vector2 randomRespawnTime = new(10f, 20f); [field: Range(0, 10)] [field: Tooltip("응집력(뭉치기) 가중치")] [field: SerializeField] public float CohesionWeight { get; private set; } = 1; [field: Range(0, 10)] [field: Tooltip("정렬(같은 방향) 가중치")] [field: SerializeField] public float AlignmentWeight { get; private set; } = 1; [field: Range(0, 10)] [field: Tooltip("분리(서로 회피) 가중치")] [field: SerializeField] public float SeparationWeight { get; private set; } = 1; [field: Range(0, 100)] [field: Tooltip("경계 범위 내 행동 가중치")] [field: SerializeField] public float BoundsWeight { get; private set; } = 1; [field: Range(0, 100)] [field: Tooltip("장애물 회피 가중치")] [field: SerializeField] public float ObstacleWeight { get; private set; } = 10; [field: Range(0, 10)] [field: Tooltip("자아(독립행동) 가중치")] [field: SerializeField] public float EgoWeight { get; private set; } = 1; // 도주 기능 설정 [Title("도주 기능 설정")] [SerializeField] private bool isDrawGizmos = true; [Tooltip("타겟 인식 범위")] [SerializeField] private float viewRadius = 10f; [Tooltip("이동속도")] [SerializeField] private float moveSpd = 500f; [Tooltip("랜덤 방향으로 도주 여부")] [SerializeField] private bool isRandomAngle = true; [ShowIf("@isRandomAngle")] [Tooltip("도망가는 방향의 랜덤 각도")] [SerializeField] private float randomAngle = 180f; [Tooltip("타겟을 재검색하는 시간")] [SerializeField] private float rescanTime = 0.5f; [Tooltip("도망치는 시간")] [SerializeField] private float escapeTime = 10f; [Tooltip("도망치면서 방향 전환 여부")] [SerializeField] private bool isDirectionChange; [ShowIf("@isDirectionChange")] [Tooltip("도망치면서 방향 전환하는데 걸리는 시간")] [SerializeField] private Vector2 randomDirectionChangeInterval = new(0.5f, 3f); [Tooltip("도주 방식")] [SerializeField] private EscapeMode escapeMode = EscapeMode.STRAIGHT; // ZIGZAG [Title("ZIGZAG")] [ShowIf("@escapeMode == EscapeMode.ZIGZAG")] [Tooltip("흔들림의 정도")] [SerializeField] private Vector2 randomZigzagAmplitude = new(0.1f, 1f); [ShowIf("@escapeMode == EscapeMode.ZIGZAG")] [Tooltip("흔들림의 주기")] [SerializeField] private Vector2 randomZigzagFrequency = new(0.1f, 1f); // Extensions Data [Title("Extensions Data")] [Tooltip("경계 범위 기능 여부")] [SerializeField] private bool showBounds; [Tooltip("FishSpot")] [SerializeField] private Transform fishSpot; [Tooltip("물 표면 이펙트 기능 여부")] [SerializeField] private bool showWaterEffect = true; [ShowIf("@showWaterEffect")] [SerializeField] private bool isUsingDynamicHeight; [ShowIf("@showWaterEffect && !isUsingDynamicHeight")] [SerializeField] private Vector3 fishSpotOffset = new(0, 0.5f, 0); [SerializeField] private GameObject contentUiPrefab; [field: SerializeField] public MeshRenderer BoundMeshRenderer { get; private set; } // 디버깅 [Title("디버깅")] [SerializeField] private List boidList; [SerializeField] private Collider[] hitColliders = new Collider[MAX_HIT_NUM]; [SerializeField] private LayerMask targetLayer; [SerializeField] private LayerMask waterLayer; private Vector3 spawnPos; private Coroutine findTargetCoroutine; private Coroutine escapeCoroutine; private WaitForSeconds findCoroutineTime; private ItemUiController contentUi; private Transform itemsLoot; private StylizedWaterDataProvider stylizedWaterDataProvider; private WaterObject waterObject; private const int MAX_HIT_NUM = 3; private void OnValidate() { if (BoundMeshRenderer) { BoundMeshRenderer.enabled = showBounds; } findCoroutineTime = new WaitForSeconds(rescanTime); } private void OnDrawGizmosSelected() { if (!isDrawGizmos || !fishSpot) return; var centerPos = Vector3.zero; if (Application.isPlaying) { centerPos = fishSpot.position; } else { if (Physics.Raycast(BoundMeshRenderer.transform.position, Vector3.up, out var hit, float.MaxValue,waterLayer)) { centerPos = hit.point + fishSpotOffset; } } Gizmos.color = Color.red; Gizmos.DrawWireSphere(centerPos, viewRadius); } private void Start() { CreateBoids(); } private void FixedUpdate() { ShowFishSpot(); } public void CreateBoids() { boidList = new List(boidCount); hitColliders = new Collider[MAX_HIT_NUM]; findCoroutineTime = new WaitForSeconds(rescanTime); BoundMeshRenderer = GetComponentInChildren(); BoundMeshRenderer.enabled = showBounds; spawnPos = BoundMeshRenderer.transform.position; targetLayer = LayerMask.GetMask("Player"); waterLayer = LayerMask.GetMask("Water"); var myTransform = transform; for (var i = 0; i < boidCount; i++) { var randomPos = Random.insideUnitSphere * spawnRange; var randomRotation = Quaternion.Euler(0, Random.Range(0, 360f), 0); var boid = Instantiate(boidPrefab, myTransform.position + randomPos, randomRotation, myTransform); boid.Init(this, Random.Range(RandomSpeedRange.x, RandomSpeedRange.y)); boidList.Add(boid); } if (fishSpot) { findTargetCoroutine ??= StartCoroutine(FindTargetCoroutine()); if (showWaterEffect) { var screenPos = CameraManager.Inst.MainCam.WorldToScreenPoint(fishSpot.position); itemsLoot = UiManager.Inst.OceanUi.MainCanvas.transform.Find("ItemsLoot"); contentUi = Instantiate(contentUiPrefab, screenPos, Quaternion.identity, itemsLoot).GetComponent(); contentUi.Init(fishSpot); } } } private IEnumerator FindTargetCoroutine() { while (true) { var size = Physics.OverlapSphereNonAlloc(fishSpot.position, viewRadius, hitColliders, targetLayer); for (var i = 0; i < size; i++) { if (hitColliders[i] == null || !hitColliders[i].CompareTag("ShipPlayer")) continue; findTargetCoroutine = null; escapeCoroutine = StartCoroutine(EscapeCoroutine(hitColliders[i])); yield break; } yield return findCoroutineTime; } } private IEnumerator EscapeCoroutine(Collider targetCollider) { var currentDirectionChangeInterval = isDirectionChange ? Random.Range(randomDirectionChangeInterval.x, randomDirectionChangeInterval.y) : 0; var rotatedEscapeDirection = CalculateEscapeDirection(targetCollider.transform.position); var currentZigzagFrequency = Random.Range(randomZigzagFrequency.x, randomZigzagFrequency.y); var currentZigzagAmplitude = Random.Range(randomZigzagAmplitude.x, randomZigzagAmplitude.y); var time = 0f; var directionChangeTime = 0f; while (time < escapeTime) { time += Time.deltaTime; if (isDirectionChange) { directionChangeTime += Time.deltaTime; if (directionChangeTime >= currentDirectionChangeInterval) { rotatedEscapeDirection = CalculateEscapeDirection(targetCollider.transform.position); directionChangeTime = 0f; currentDirectionChangeInterval = Random.Range(randomDirectionChangeInterval.x,randomDirectionChangeInterval.y); if (escapeMode == EscapeMode.ZIGZAG) { currentZigzagFrequency = Random.Range(randomZigzagFrequency.x, randomZigzagFrequency.y); currentZigzagAmplitude = Random.Range(randomZigzagAmplitude.x, randomZigzagAmplitude.y); } } } var newDirection = escapeMode switch { EscapeMode.NONE => throw new ArgumentOutOfRangeException(), EscapeMode.STRAIGHT => rotatedEscapeDirection, EscapeMode.ZIGZAG => rotatedEscapeDirection + new Vector3(Mathf.Sin(Time.time * currentZigzagFrequency) * currentZigzagAmplitude,0, Mathf.Sin(Time.time * currentZigzagFrequency) * currentZigzagAmplitude), EscapeMode.TOWARDS => -rotatedEscapeDirection, _ => throw new ArgumentOutOfRangeException() }; BoundMeshRenderer.transform.position += newDirection.normalized * (moveSpd * Time.deltaTime); yield return null; } escapeCoroutine = null; while (boidList.Count > 0) { var currentBoid = boidList[0]; boidList.RemoveAt(0); Destroy(currentBoid.gameObject); } if (isAutoRespawn) { BoidsManager.Inst.RespawnBoids(this, Random.Range(randomRespawnTime.x, randomRespawnTime.y), spawnPos); } gameObject.SetActive(false); Destroy(contentUi.gameObject); } private Vector3 CalculateEscapeDirection(Vector3 targetPos) { var escapeDirection = (transform.position - targetPos).normalized; escapeDirection.y = 0; if (!isRandomAngle) return escapeDirection; var randomRotationAngle = Random.Range(-randomAngle * 0.5f, randomAngle * 0.5f); var rotation = Quaternion.Euler(0, randomRotationAngle, 0); return rotation * escapeDirection; } public void CatchBoid(Collider hitCollider, int count) { count = Mathf.Min(count, boidList.Count); for (var i = 0; i < count; i++) { // 물고기 잡히는 이펙트 효과 추가 var currentBoid = boidList[0]; var bounds = hitCollider.bounds; var x = Random.Range(bounds.min.x, bounds.max.x); //var y = Random.Range(bounds.min.y, bounds.max.y); var z = Random.Range(bounds.min.z, bounds.max.z); var randomPos = new Vector3(x, 0, z); var catchItem = new FishItem(currentBoid.FishItem.ItemName, currentBoid.FishItem.ItemCount, currentBoid.FishItem.ItemIcon); ItemDropManager.Inst.DropItem(catchItem, randomPos); boidList.RemoveAt(0); Destroy(currentBoid.gameObject); } if (boidList.Count > 0) return; if (isAutoRespawn) { BoidsManager.Inst.RespawnBoids(this, Random.Range(randomRespawnTime.x, randomRespawnTime.y), spawnPos); } gameObject.SetActive(false); Destroy(contentUi.gameObject); } private void ShowFishSpot() { if (!fishSpot || !showWaterEffect) return; if (Physics.Raycast(BoundMeshRenderer.transform.position, Vector3.up, out var hit, float.MaxValue,waterLayer)) { if (stylizedWaterDataProvider == null || waterObject == null) { stylizedWaterDataProvider = hit.collider.GetComponent(); waterObject = stylizedWaterDataProvider.GetComponent(); } if (isUsingDynamicHeight) { var newFishSpotPosition = fishSpot.position; newFishSpotPosition.y = stylizedWaterDataProvider.GetWaterHeightSingle(waterObject, hit.point);; fishSpot.position = newFishSpotPosition; } else { fishSpot.position = hit.point + fishSpotOffset; } } } public FishItem GetBoidInfo() => boidPrefab.FishItem; } }