2024-01-02 21:39:53 +00:00
|
|
|
using System;
|
2023-12-29 05:59:04 +00:00
|
|
|
using System.Collections;
|
2024-01-02 21:39:53 +00:00
|
|
|
using System.Collections.Generic;
|
2024-01-31 06:18:27 +00:00
|
|
|
using NWH.DWP2.WaterData;
|
|
|
|
using NWH.DWP2.WaterObjects;
|
2023-12-28 07:29:52 +00:00
|
|
|
using Sirenix.OdinInspector;
|
|
|
|
using UnityEngine;
|
|
|
|
using Random = UnityEngine.Random;
|
|
|
|
|
|
|
|
// ReSharper disable once CheckNamespace
|
|
|
|
namespace BlueWaterProject
|
|
|
|
{
|
2024-01-02 21:39:53 +00:00
|
|
|
public enum EscapeMode
|
|
|
|
{
|
|
|
|
NONE = -1,
|
|
|
|
STRAIGHT,
|
|
|
|
ZIGZAG,
|
|
|
|
TOWARDS
|
|
|
|
}
|
|
|
|
|
2023-12-28 07:29:52 +00:00
|
|
|
public class Boids : MonoBehaviour
|
|
|
|
{
|
2024-01-02 21:39:53 +00:00
|
|
|
// 군집(떼) 설정
|
2023-12-28 07:29:52 +00:00
|
|
|
[Title("군집(떼) 설정")]
|
|
|
|
[Tooltip("Boid 프리팹")]
|
|
|
|
[SerializeField] private Boid boidPrefab;
|
|
|
|
|
|
|
|
[Range(1, 1000)]
|
|
|
|
[Tooltip("생성할 개체 수")]
|
|
|
|
[SerializeField] private int boidCount = 5;
|
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
[field: Tooltip("개체의 랜덤 속도 값\nx == Min\ny == Max")]
|
|
|
|
[field: SerializeField] public Vector2 RandomSpeedRange { get; private set; } = new(5f, 10f);
|
|
|
|
|
2023-12-28 07:29:52 +00:00
|
|
|
[Range(5, 100)]
|
|
|
|
[Tooltip("개체 생성 범위")]
|
|
|
|
[SerializeField] private float spawnRange = 10;
|
2024-01-02 21:39:53 +00:00
|
|
|
|
|
|
|
[Tooltip("자동 재생성 기능 여부")]
|
|
|
|
[SerializeField] private bool isAutoRespawn = true;
|
2023-12-28 07:29:52 +00:00
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
[Tooltip("자동 재생성하는데 걸리는 시간")]
|
|
|
|
[ShowIf("@isAutoRespawn")]
|
|
|
|
[SerializeField] private Vector2 randomRespawnTime = new(10f, 20f);
|
2023-12-28 07:29:52 +00:00
|
|
|
|
|
|
|
[field: Range(0, 10)]
|
2023-12-29 05:59:04 +00:00
|
|
|
[field: Tooltip("응집력(뭉치기) 가중치")]
|
2023-12-28 07:29:52 +00:00
|
|
|
[field: SerializeField] public float CohesionWeight { get; private set; } = 1;
|
|
|
|
|
|
|
|
[field: Range(0, 10)]
|
2023-12-29 05:59:04 +00:00
|
|
|
[field: Tooltip("정렬(같은 방향) 가중치")]
|
2023-12-28 07:29:52 +00:00
|
|
|
[field: SerializeField] public float AlignmentWeight { get; private set; } = 1;
|
|
|
|
|
|
|
|
[field: Range(0, 10)]
|
2023-12-29 05:59:04 +00:00
|
|
|
[field: Tooltip("분리(서로 회피) 가중치")]
|
2023-12-28 07:29:52 +00:00
|
|
|
[field: SerializeField] public float SeparationWeight { get; private set; } = 1;
|
|
|
|
|
|
|
|
[field: Range(0, 100)]
|
2023-12-29 05:59:04 +00:00
|
|
|
[field: Tooltip("경계 범위 내 행동 가중치")]
|
2023-12-28 07:29:52 +00:00
|
|
|
[field: SerializeField] public float BoundsWeight { get; private set; } = 1;
|
|
|
|
|
|
|
|
[field: Range(0, 100)]
|
2023-12-29 05:59:04 +00:00
|
|
|
[field: Tooltip("장애물 회피 가중치")]
|
2023-12-28 07:29:52 +00:00
|
|
|
[field: SerializeField] public float ObstacleWeight { get; private set; } = 10;
|
|
|
|
|
|
|
|
[field: Range(0, 10)]
|
2023-12-29 05:59:04 +00:00
|
|
|
[field: Tooltip("자아(독립행동) 가중치")]
|
2023-12-28 07:29:52 +00:00
|
|
|
[field: SerializeField] public float EgoWeight { get; private set; } = 1;
|
2024-01-02 21:39:53 +00:00
|
|
|
|
|
|
|
// 도주 기능 설정
|
|
|
|
[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("경계 범위 기능 여부")]
|
2023-12-28 07:29:52 +00:00
|
|
|
[SerializeField] private bool showBounds;
|
|
|
|
|
2024-01-17 16:00:48 +00:00
|
|
|
[Tooltip("FishSpot")]
|
|
|
|
[SerializeField] private Transform fishSpot;
|
2024-01-02 21:39:53 +00:00
|
|
|
|
|
|
|
[Tooltip("물 표면 이펙트 기능 여부")]
|
|
|
|
[SerializeField] private bool showWaterEffect = true;
|
2024-01-31 06:18:27 +00:00
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
[ShowIf("@showWaterEffect")]
|
2024-01-31 06:18:27 +00:00
|
|
|
[SerializeField] private bool isUsingDynamicHeight;
|
|
|
|
|
|
|
|
[ShowIf("@showWaterEffect && !isUsingDynamicHeight")]
|
2024-01-17 16:00:48 +00:00
|
|
|
[SerializeField] private Vector3 fishSpotOffset = new(0, 0.5f, 0);
|
2024-01-02 21:39:53 +00:00
|
|
|
|
2024-01-22 12:12:55 +00:00
|
|
|
[SerializeField] private GameObject contentUiPrefab;
|
|
|
|
|
2024-01-21 17:42:31 +00:00
|
|
|
[field: SerializeField] public MeshRenderer BoundMeshRenderer { get; private set; }
|
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
// 디버깅
|
|
|
|
[Title("디버깅")]
|
|
|
|
[SerializeField] private List<Boid> boidList;
|
|
|
|
[SerializeField] private Collider[] hitColliders = new Collider[MAX_HIT_NUM];
|
|
|
|
[SerializeField] private LayerMask targetLayer;
|
|
|
|
[SerializeField] private LayerMask waterLayer;
|
2023-12-28 07:29:52 +00:00
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
private Vector3 spawnPos;
|
|
|
|
private Coroutine findTargetCoroutine;
|
|
|
|
private Coroutine escapeCoroutine;
|
|
|
|
private WaitForSeconds findCoroutineTime;
|
2024-01-22 12:12:55 +00:00
|
|
|
private ItemUiController contentUi;
|
|
|
|
private Transform itemsLoot;
|
2024-01-31 06:18:27 +00:00
|
|
|
private StylizedWaterDataProvider stylizedWaterDataProvider;
|
|
|
|
private WaterObject waterObject;
|
2024-01-02 21:39:53 +00:00
|
|
|
|
|
|
|
private const int MAX_HIT_NUM = 3;
|
|
|
|
|
2023-12-28 07:29:52 +00:00
|
|
|
private void OnValidate()
|
|
|
|
{
|
|
|
|
if (BoundMeshRenderer)
|
|
|
|
{
|
|
|
|
BoundMeshRenderer.enabled = showBounds;
|
|
|
|
}
|
2024-01-02 21:39:53 +00:00
|
|
|
|
|
|
|
findCoroutineTime = new WaitForSeconds(rescanTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void OnDrawGizmosSelected()
|
|
|
|
{
|
2024-01-31 06:18:27 +00:00
|
|
|
if (!isDrawGizmos || !fishSpot) return;
|
2024-01-21 17:42:31 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2024-01-02 21:39:53 +00:00
|
|
|
|
|
|
|
Gizmos.color = Color.red;
|
2024-01-21 17:42:31 +00:00
|
|
|
Gizmos.DrawWireSphere(centerPos, viewRadius);
|
2023-12-28 07:29:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void Start()
|
|
|
|
{
|
|
|
|
CreateBoids();
|
|
|
|
}
|
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
private void FixedUpdate()
|
2023-12-28 07:29:52 +00:00
|
|
|
{
|
2024-01-31 06:18:27 +00:00
|
|
|
ShowFishSpot();
|
2024-01-02 21:39:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void CreateBoids()
|
|
|
|
{
|
|
|
|
boidList = new List<Boid>(boidCount);
|
|
|
|
hitColliders = new Collider[MAX_HIT_NUM];
|
|
|
|
findCoroutineTime = new WaitForSeconds(rescanTime);
|
2023-12-28 07:29:52 +00:00
|
|
|
BoundMeshRenderer = GetComponentInChildren<MeshRenderer>();
|
|
|
|
BoundMeshRenderer.enabled = showBounds;
|
2024-01-02 21:39:53 +00:00
|
|
|
spawnPos = BoundMeshRenderer.transform.position;
|
|
|
|
targetLayer = LayerMask.GetMask("Player");
|
|
|
|
waterLayer = LayerMask.GetMask("Water");
|
|
|
|
|
2023-12-28 07:29:52 +00:00
|
|
|
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));
|
2024-01-02 21:39:53 +00:00
|
|
|
boidList.Add(boid);
|
2023-12-28 07:29:52 +00:00
|
|
|
}
|
2024-01-22 12:12:55 +00:00
|
|
|
|
2024-01-31 06:18:27 +00:00
|
|
|
if (fishSpot)
|
2024-01-22 12:12:55 +00:00
|
|
|
{
|
2024-01-31 06:18:27 +00:00
|
|
|
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<ItemUiController>();
|
|
|
|
contentUi.Init(fishSpot);
|
|
|
|
}
|
2024-01-22 12:12:55 +00:00
|
|
|
}
|
2023-12-28 07:29:52 +00:00
|
|
|
}
|
2024-01-02 21:39:53 +00:00
|
|
|
|
|
|
|
private IEnumerator FindTargetCoroutine()
|
|
|
|
{
|
|
|
|
while (true)
|
|
|
|
{
|
2024-01-21 17:42:31 +00:00
|
|
|
var size = Physics.OverlapSphereNonAlloc(fishSpot.position, viewRadius, hitColliders, targetLayer);
|
2024-01-02 21:39:53 +00:00
|
|
|
for (var i = 0; i < size; i++)
|
|
|
|
{
|
|
|
|
if (hitColliders[i] == null || !hitColliders[i].CompareTag("ShipPlayer")) continue;
|
2023-12-29 05:59:04 +00:00
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
findTargetCoroutine = null;
|
|
|
|
escapeCoroutine = StartCoroutine(EscapeCoroutine(hitColliders[i]));
|
|
|
|
yield break;
|
|
|
|
}
|
|
|
|
yield return findCoroutineTime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private IEnumerator EscapeCoroutine(Collider targetCollider)
|
2023-12-29 05:59:04 +00:00
|
|
|
{
|
2024-01-02 21:39:53 +00:00
|
|
|
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)
|
2023-12-29 05:59:04 +00:00
|
|
|
{
|
2024-01-02 21:39:53 +00:00
|
|
|
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);
|
2023-12-29 05:59:04 +00:00
|
|
|
yield return null;
|
|
|
|
}
|
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
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);
|
2024-01-22 12:12:55 +00:00
|
|
|
Destroy(contentUi.gameObject);
|
2024-01-02 21:39:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-01-21 17:42:31 +00:00
|
|
|
public void CatchBoid(Collider hitCollider, int count)
|
2024-01-02 21:39:53 +00:00
|
|
|
{
|
|
|
|
count = Mathf.Min(count, boidList.Count);
|
|
|
|
for (var i = 0; i < count; i++)
|
|
|
|
{
|
|
|
|
// 물고기 잡히는 이펙트 효과 추가
|
|
|
|
var currentBoid = boidList[0];
|
2024-01-21 17:42:31 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2024-01-22 12:12:55 +00:00
|
|
|
var catchItem = new FishItem(currentBoid.FishItem.ItemName, currentBoid.FishItem.ItemCount, currentBoid.FishItem.ItemIcon);
|
2024-01-21 17:42:31 +00:00
|
|
|
ItemDropManager.Inst.DropItem(catchItem, randomPos);
|
|
|
|
|
2024-01-02 21:39:53 +00:00
|
|
|
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);
|
2024-01-22 12:12:55 +00:00
|
|
|
Destroy(contentUi.gameObject);
|
2023-12-29 05:59:04 +00:00
|
|
|
}
|
2024-01-21 17:42:31 +00:00
|
|
|
|
2024-01-31 06:18:27 +00:00
|
|
|
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<StylizedWaterDataProvider>();
|
|
|
|
waterObject = stylizedWaterDataProvider.GetComponent<WaterObject>();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isUsingDynamicHeight)
|
|
|
|
{
|
|
|
|
var newFishSpotPosition = fishSpot.position;
|
|
|
|
newFishSpotPosition.y = stylizedWaterDataProvider.GetWaterHeightSingle(waterObject, hit.point);;
|
|
|
|
fishSpot.position = newFishSpotPosition;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
fishSpot.position = hit.point + fishSpotOffset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-21 17:42:31 +00:00
|
|
|
public FishItem GetBoidInfo() => boidPrefab.FishItem;
|
2023-12-28 07:29:52 +00:00
|
|
|
}
|
|
|
|
}
|