using System;
using UnityEngine;
using UnityEngine.AI;
// ReSharper disable once CheckNamespace
namespace BlueWaterProject
{
public class AiWeight : MonoBehaviour
{
[Serializable]
private struct DirectionInfo
{
public bool hit; // 장애물 충돌 여부
public Vector3 direction; // 방향 노말값
public float interest; // 호기심 수치
public float danger; // 위험 수치
public DirectionInfo(bool hit, Vector3 direction, float interest, float danger)
{
this.hit = hit;
this.direction = direction;
this.interest = interest;
this.danger = danger;
}
}
[field: Tooltip("Scene뷰에서의 가중치 시스템 가시화 여부")]
[field: SerializeField] private bool drawGizmo = true;
[field: Tooltip("target과의 거리가 설정한 값보다 큰 경우 일직선으로 이동")]
[field: SerializeField] private float maxDistance = 10f;
[field: Tooltip("target과의 거리가 설정한 값보다 큰 경우 가중치 시스템에 의해 이동")]
[field: SerializeField] private float minDistance = 3f;
[field: Tooltip("target과의 거리가 설정한 값보다 큰 경우 target을 중심으로 공전")]
[field: SerializeField] private float orbitDistance = 2f;
[field: Tooltip("공전 허용 범위\norbitDistance < target < orbitDistance + 설정 값")]
[field: SerializeField] private float orbitAllowableValue = 2f;
[field: Tooltip("가중치 시스템 장애물에 해당되는 레이어 설정")]
[field: SerializeField] private LayerMask obstacleWeightLayer;
[field: Tooltip("장애물 최대 인지 거리")]
[field: SerializeField] private float obstacleDist = 5f;
[field: Tooltip("방향이 가지는 구조체의 배열")]
[field: SerializeField] private DirectionInfo[] directionInfo = new DirectionInfo[24];
[field: Tooltip("현재 공전 중일 때, 해당 방향의 위험 수치가 설정한 값 이상일 경우, 반대편 회전")]
[field: SerializeField] private float dangerValue = 0.8f;
[field: Header("Target Raycast")]
[field: Tooltip("목표 레이어 설정")]
[field: SerializeField] private LayerMask targetLayer;
[field: Tooltip("목표와의 장애물에 해당되는 레이어 설정")]
[field: SerializeField] private LayerMask obstacleTargetLayer;
[field: Tooltip("현재 공전 중일 때, 시계 방향으로 회전 중인지 여부")]
[field: SerializeField] public bool isClockwise;
[field: Tooltip("현재 공전 중인지 여부")]
[field: SerializeField] public bool beInOrbit;
[field: Tooltip("현재 회피 중인지 여부")]
[field: SerializeField] public bool isAvoiding;
private bool usedWeighted;
private void Reset()
{
drawGizmo = true;
maxDistance = 10f;
minDistance = 3f;
orbitDistance = 2f;
orbitAllowableValue = 2f;
obstacleDist = 5f;
directionInfo = new DirectionInfo[24];
dangerValue = 0.8f;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (!drawGizmo || !usedWeighted) return;
for (var i = 0; i < directionInfo.Length; i++)
{
var myPos = transform.position;
Gizmos.color = GetFinalDirectionValue().direction == directionInfo[i].direction
? Color.green
: Color.white;
var resultWeighted = Mathf.Clamp(directionInfo[i].interest - directionInfo[i].danger, 0f, 1f);
Gizmos.DrawLine(myPos, myPos + directionInfo[i].direction * resultWeighted);
}
}
#endif
private void Awake()
{
var angle = 360f / directionInfo.Length;
for (var i = 0; i < directionInfo.Length; i++)
{
var direction = Utils.AngleToDir(angle * i + 5);
directionInfo[i] = new DirectionInfo(false, direction, 0, 0);
}
}
///
/// Update()에서 사용하는 Enemy의 가중치 시스템
///
public void UpdateWeighedEnemy(NavMeshAgent agent, Transform targetTransform,
Transform rotationTransform, bool canLookAtTarget, float distanceToTarget, Vector3 directionToTarget)
{
var resultDirection = SetResultDirection(directionToTarget);
CheckObstacle();
resultDirection = CheckTurnAround(resultDirection, directionToTarget);
// raycast가 장애물에 막혔을 경우, agent 사용
if (Physics.Raycast(transform.position, directionToTarget, distanceToTarget, obstacleTargetLayer))
{
usedWeighted = false;
MoveTowardDirection(agent, targetTransform.position, targetTransform, rotationTransform,
canLookAtTarget);
return;
}
usedWeighted = true;
SetWeights(distanceToTarget, directionToTarget, resultDirection);
MoveTowardDirection(agent, transform.position + CalcDirectionInfo().direction, targetTransform,
rotationTransform, canLookAtTarget);
}
///
/// 방향별로 장애물 체크 및 위험 수치 계산 함수
///
private void CheckObstacle()
{
for (var i = 0; i < directionInfo.Length; i++)
{
if (Physics.Raycast(transform.position, directionInfo[i].direction, out var hit, obstacleDist,
obstacleWeightLayer))
{
var obstacleFactor = (obstacleDist - hit.distance) / obstacleDist;
directionInfo[i].danger = obstacleFactor;
directionInfo[i].hit = true;
}
else
{
directionInfo[i].danger = 0f;
directionInfo[i].hit = false;
}
}
}
///
/// 방향별로 호기심 수치 계산 함수
///
private void SetWeights(float distanceToTarget, Vector3 directionToTarget, Vector3 resultDir)
{
// target이 raycast로 닿았다면, 가중치 시스템 + 공전 + 회피 이동
for (var i = 0; i < directionInfo.Length; i++)
{
if (distanceToTarget > maxDistance)
{
usedWeighted = false;
SetBeInOrBitAndIsAvoiding(false, false);
if (Vector3.Dot(directionInfo[i].direction, directionToTarget) > 0.7f)
{
directionInfo[i].interest = 1f;
}
}
else if ((!beInOrbit && distanceToTarget > minDistance) ||
(beInOrbit && distanceToTarget > orbitDistance + orbitAllowableValue))
{
usedWeighted = true;
SetBeInOrBitAndIsAvoiding(false, false);
directionInfo[i].interest =
Mathf.Clamp(Vector3.Dot(directionInfo[i].direction, directionToTarget), 0, 1)
- directionInfo[i].danger;
}
else if (distanceToTarget > orbitDistance)
{
usedWeighted = true;
SetBeInOrBitAndIsAvoiding(true, false);
directionInfo[i].interest = Mathf.Clamp(Vector3.Dot(directionInfo[i].direction, resultDir), 0, 1)
- directionInfo[i].danger;
}
else if (distanceToTarget <= orbitDistance)
{
usedWeighted = true;
SetBeInOrBitAndIsAvoiding(false, true);
directionInfo[i].interest =
Mathf.Clamp(Vector3.Dot(directionInfo[i].direction, -directionToTarget), 0, 1);
}
}
}
///
/// 공전 중이며, 공전 하는 방향의 위험 수치가 일정 수치 이상일 때, 반대 방향으로 회전하는 함수
///
private Vector3 CheckTurnAround(Vector3 resultDir, Vector3 directionToTarget)
{
if (beInOrbit || directionInfo[GetDirectionIndex(resultDir)].danger < dangerValue) return resultDir;
isClockwise = !isClockwise;
return SetResultDirection(directionToTarget);
}
///
/// target을 쳐다보는지의 여부
///
///
///
///
private void LookAtTarget(Transform targetTransform, Transform rotationTransform, bool canLookAtTarget)
{
var targetPosition = targetTransform.position;
targetPosition.y = transform.position.y;
if (canLookAtTarget)
{
rotationTransform.LookAt(targetPosition);
}
}
///
/// 가중치가 가장 높은 방향으로 이동시키는 함수
///
/// NavMeshAgent Component
/// 최종 이동할 위치
/// 목표 객체 : TargetTransform
/// 회전 객체 : RotationTransform
/// 일정 거리 안에 Target이 있다면 true를 반환
private void MoveTowardDirection(NavMeshAgent agent, Vector3 resultDirection, Transform targetTransform,
Transform rotationTransform, bool canLookAtTarget)
{
LookAtTarget(targetTransform, rotationTransform, canLookAtTarget);
agent.SetDestination(resultDirection);
}
///
/// 호기심 수치가 가장 높은 방향의 정보를 받는 함수
///
private DirectionInfo CalcDirectionInfo()
{
var bestDirectionIndex = 0;
var bestWeight = 0f;
for (var i = 0; i < directionInfo.Length; i++)
{
if (directionInfo[i].interest <= bestWeight) continue;
bestWeight = directionInfo[i].interest;
bestDirectionIndex = i;
}
return directionInfo[bestDirectionIndex];
}
///
/// 호기심 수치가 가장 높은 방향의 정보를 받는 함수
///
private DirectionInfo GetFinalDirectionValue()
{
var bestDirectionIndex = 0;
var bestWeight = 0f;
for (var i = 0; i < directionInfo.Length; i++)
{
var finalInterestValue = Mathf.Clamp(directionInfo[i].interest - directionInfo[i].danger, 0f, 1f);
if (finalInterestValue <= bestWeight) continue;
bestWeight = finalInterestValue;
bestDirectionIndex = i;
}
return directionInfo[bestDirectionIndex];
}
///
/// 매개변수의 방향값과 가장 인접한 방향의 Index를 반환하는 함수
///
private int GetDirectionIndex(Vector3 direction)
{
var maxDotValue = 0f;
var index = 0;
for (var i = 0; i < directionInfo.Length; i++)
{
var dotValue = Vector3.Dot(directionInfo[i].direction, direction);
if (maxDotValue >= dotValue) continue;
maxDotValue = dotValue;
index = i;
}
return index;
}
///
/// 공전하는 방향을 설정하는 함수
/// target과 수직인 시계방향 : new Vector3(-directionToTarget.z, 0f, directionToTarget.x)
/// target과 수직인 반시계방향 : new Vector3(directionToTarget.z, 0f, -directionToTarget.x)
///
private Vector3 SetResultDirection(Vector3 directionToTarget)
{
return isClockwise
? new Vector3(-directionToTarget.z, 0f, directionToTarget.x)
: new Vector3(directionToTarget.z, 0f, -directionToTarget.x);
}
private void SetBeInOrBitAndIsAvoiding(bool beInOrbitValue, bool isAvoidingValue)
{
beInOrbit = beInOrbitValue;
isAvoiding = isAvoidingValue;
}
}
}