AI Vision & Sound Sensor
Player 나 Object 를 Detect 하는 AISensor 모듈을 만들어 보았다.
두 가지 방법이 떠올랐는데,
- Mesh Collider 의 Trigger 이벤트 사용
- 원뿔형으로 LineCast 를 쏴서 RaycastHit 로 Detect
그러나 첫번째 방법에는 문제점이 있다.
- LayerMask 를 활용할 수 없다.
- 그렇기 때문에 장애물이 센서 시야를 가리게 만들 수 없다.
최종적으로 두번째 방법을 사용했다.
AIVisionSensor
public class AIVisionSensor : MonoBehaviour
{
public float redZoneDistance = 8.5f;
public float yellowZoneDistance = 12.5f;
[Header("Angle")]
[Range(0, 180)]
public float horizontalAngle = 20;
[Range(0, 180)]
public float verticalAngle = 20;
[Header("Resolution")]
[Range(0, 48)]
public int horizontalResolution = 10;
[Range(0, 48)]
public int verticalResolution = 3;
[Header("Interval")]
public float scanInterval = 0.5f;
private float scanTimer;
[Header("Result")]
public List<GameObject> yellowZoneObjectList = new();
public List<GameObject> redZoneObjectList = new();
private void Ray(float currentVAngle, float currentHAngle, int v, int h, float distance, List<GameObject> objectList)
{
Vector3 point = transform.position +
Quaternion.AngleAxis(currentVAngle, transform.right) * Quaternion.AngleAxis(currentHAngle, transform.up) * transform.forward * distance;
Physics.Linecast(transform.position, point, out RaycastHit hit, 1 << LayerMask.NameToLayer("Obstacle") | 1 << LayerMask.NameToLayer("Object") | 1 << LayerMask.NameToLayer("Ground"));
if ((h == 0 || h == horizontalResolution - 1) && (v == 0 || v == verticalResolution - 1))
{
Debug.DrawLine(transform.position, hit.collider ? hit.point : point, Color.white, scanInterval);
}
DebugExtension.DebugPoint(point, Color.white, 0.25f, scanInterval);
if (hit.collider && hit.collider.gameObject.layer == LayerMask.NameToLayer("Object"))
{
var detectedObject = hit.collider.gameObject;
if (!objectList.Contains(detectedObject)) objectList.Add(detectedObject);
}
}
private void Scan()
{
yellowZoneObjectList.Clear();
redZoneObjectList.Clear();
float currentVAngle = -verticalAngle;
float deltaVAngle = (verticalAngle * 2) / (verticalResolution - 1);
for (int v = 0; v < verticalResolution; v++)
{
float currentHAngle = -horizontalAngle;
float deltaHAngle = (horizontalAngle * 2) / (horizontalResolution - 1);
for (int h = 0; h < horizontalResolution; h++)
{
Ray(currentVAngle, currentHAngle, v, h, yellowZoneDistance, yellowZoneObjectList);
Ray(currentVAngle, currentHAngle, v, h, redZoneDistance, redZoneObjectList);
currentHAngle += deltaHAngle;
}
currentVAngle += deltaVAngle;
}
}
private void FixedUpdate()
{
scanTimer -= Time.deltaTime;
if (scanTimer < 0)
{
scanTimer += scanInterval;
Scan();
}
}
}
파라미터
- 위 그림에서 수평 각도가 Horizontal Angle, 수직 각도가 Vertical Angle 값이다.
- Resolution 은 쏘는 Line 의 개수이므로 해상도를 의미한다.
아래와 같이 파라미터를 조절해 해상도와 각도를 조절할 수 있다.
Scan 함수
Obstacle 레이어와 Object 레이어의 물체를 LineCast 하고, 그 중 Object 레이어의 물체만 리턴한다.
아래 그림과 같이 장애물이 있을 경우에는 LineCast 가 통과하지 못한다.
플레이어가 장애물 뒤에 있었다면 Detect 되지 않았을 것이다.
RedZone / YellowZone
redZoneDistance 까지가 redZone,
redZoneDistance ~ yellowZoneDistance 까지가 yellowZone 이다.
Detect Level 을 나눠두어서 나중에 AI 구현 시 편리하도록 했다.
AISoundSensor
AISoundSensor 스크립트 구현
hearRange : [Listener] 소리를 들을 수 있는 최대 반경
soundRange : [Owner] 소리의 최대 반경
(hearRange + soundRange >= Listener 와 Owner 의 거리) 면, 소리를 Detect 했다고 판단한다.
public class AISoundSensor : MonoBehaviour
{
private EnemyRobotAI ai;
public float hearRange = 20f;
[Header("Result")]
public Vector3 lastDetectedPosition;
public GameObject lastDetectedOwner;
private void Start()
{
ai = transform.root.GetComponent<EnemyRobotAI>();
}
public void OnSoundHear(float soundRange, Vector3 soundPosition, GameObject owner)
{
if (Vector3.Distance(transform.position, soundPosition) > soundRange + hearRange) return;
lastDetectedPosition = soundPosition;
lastDetectedOwner = owner;
StartCoroutine(ai.SoundReaction(soundPosition));
}
}
GunController 수정
FireWeapon 함수가 성공적으로 실행되면 'Robot' 태그를 가진 모든 GameObject 의 OnSoundHear 함수를 호출한다.
// GunController.cs
private void FireWeapon()
{
...
foreach (var robot in GameObject.FindGameObjectsWithTag("Robot"))
{
if (robot == transform.root.gameObject) continue;
var soundSensor = robot.GetComponentInChildren<AISoundSensor>();
if (soundSensor) soundSensor.OnSoundHear(fireSoundRange, transform.position, transform.root.gameObject);
}
...
}