using System; using UnityEngine; using UnityEngine.AI; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public class AiWeight : MonoBehaviour { /*********************************************************************** * Definitions ***********************************************************************/ #region Definitions [Serializable] private struct DirectionInfo { public RaycastHit RaycastHit { get; set; } [field: SerializeField] public Vector3 Direction { get; set; } [field: SerializeField] public float Interest { get; set; } [field: SerializeField] public float Danger { get; set; } public DirectionInfo(RaycastHit raycastHit, Vector3 direction, float interest, float danger) { RaycastHit = raycastHit; Direction = direction; Interest = interest; Danger = danger; } } #endregion /*********************************************************************** * Variables ***********************************************************************/ #region Variables [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; [Range(1, 36), Tooltip("방향의 갯수")] [SerializeField] private int directionIndex = 24; public int DirectionIndex { get => directionIndex; set { directionIndex = value; directionInfo = new DirectionInfo[DirectionIndex]; var angle = 360f / directionInfo.Length; for (var i = 0; i < directionInfo.Length; i++) { var direction = Utils.AngleToDir(angle * i); directionInfo[i] = new DirectionInfo(default, direction, 0, 0); } } } [field: Tooltip("방향이 가지는 구조체의 배열")] [field: SerializeField] private DirectionInfo[] directionInfo; [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; #endregion /*********************************************************************** * Unity Methods ***********************************************************************/ #region Unity Methods #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 Update() { GetDirectionRaycastHit(transform.position); } private void GetDirectionRaycastHit(Vector3 startPosition) { // 각 방향에 대한 raycastHit 정보 받아두기 for (var i = 0; i < directionInfo.Length; i++) { Physics.Raycast(startPosition, directionInfo[i].Direction, out var hit, obstacleDist,obstacleWeightLayer, QueryTriggerInteraction.Ignore); directionInfo[i].RaycastHit = hit; } } #endregion /*********************************************************************** * Methods ***********************************************************************/ #region Methods /// /// 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; } /// /// 각도를 통해 방향값을 반환하는 함수 /// private Vector3 AngleToDir(float angle) { var radian = angle * Mathf.Deg2Rad; return new Vector3(Mathf.Sin(radian), 0f, Mathf.Cos(radian)).normalized; } #endregion } }