OldBlueWater/BlueWater/Assets/02.Scripts/Boids.cs
NTG 2da1b44a53 Closes #70 #79 #87
+ Layer Boids, Boid로 분리
+ Fish의 로직을 Boids(군집) 알고리즘으로 변경
+ 군집에 이펙트를 통해 낚시 가시성 추가
+ 테스트용 군집 애니메이션 추가
+ Epic Toon EX 에셋의 스크립트 수정 버전
  ㄴ ParticleWeapon(Layer 선택, UnityEvent Hit 델리게이트 기능 추가) 사용
+ Cannon의 x축 회전 고정
+ Cannon이 공격한 Layer에 따른 기능 변경
+ DataManager에 PlayerInventory 추가
  ㄴ 초창기에 사용한 코딩 삭제
+ 초창기에 사용했던 스크립트들 일부 정리
2024-01-03 06:39:53 +09:00

322 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Serialization;
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("물 표면 이펙트")]
[SerializeField] private ParticleSystem waterEffect;
[Tooltip("물 표면 이펙트 기능 여부")]
[SerializeField] private bool showWaterEffect = true;
[Tooltip("물 표면 이펙트 기능 여부")]
[ShowIf("@showWaterEffect")]
[SerializeField] private Vector3 waterEffectOffset = new(0, 0.5f, 0);
// 디버깅
[Title("디버깅")]
[SerializeField] private List<Boid> boidList;
[SerializeField] private Collider[] hitColliders = new Collider[MAX_HIT_NUM];
[SerializeField] private LayerMask targetLayer;
[SerializeField] private LayerMask waterLayer;
public MeshRenderer BoundMeshRenderer { get; private set; }
private Vector3 spawnPos;
private Coroutine findTargetCoroutine;
private Coroutine escapeCoroutine;
private WaitForSeconds findCoroutineTime;
private const int MAX_HIT_NUM = 3;
private void OnValidate()
{
if (BoundMeshRenderer)
{
BoundMeshRenderer.enabled = showBounds;
}
findCoroutineTime = new WaitForSeconds(rescanTime);
if (showWaterEffect)
{
waterEffect.Play();
}
else
{
waterEffect.Stop();
}
}
private void OnDrawGizmosSelected()
{
if (!isDrawGizmos) return;
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, viewRadius);
}
private void Start()
{
CreateBoids();
}
private void FixedUpdate()
{
if (!showWaterEffect) return;
if (Physics.Raycast(BoundMeshRenderer.transform.position, Vector3.up, out var hit,
float.MaxValue,waterLayer))
{
waterEffect.transform.position = hit.point + waterEffectOffset;
}
}
public void CreateBoids()
{
boidList = new List<Boid>(boidCount);
hitColliders = new Collider[MAX_HIT_NUM];
findCoroutineTime = new WaitForSeconds(rescanTime);
BoundMeshRenderer = GetComponentInChildren<MeshRenderer>();
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);
}
findTargetCoroutine ??= StartCoroutine(FindTargetCoroutine());
}
private IEnumerator FindTargetCoroutine()
{
while (true)
{
var size = Physics.OverlapSphereNonAlloc(transform.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);
}
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(int count)
{
count = Mathf.Min(count, boidList.Count);
for (var i = 0; i < count; i++)
{
// 물고기 잡히는 이펙트 효과 추가
var currentBoid = boidList[0];
DataManager.Inst.PlayerInventory.AddFish(currentBoid.FishInfo.Name, currentBoid.FishInfo.Count);
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);
}
}
}