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; } } }