Unity3D 적 탐지 시스템 구현
게임 내에서 적 위치를 시각적으로 탐지하고 표시하는 레이더 시스템 구현
원래는 구형 전투기 레이저 처럼 탐지 막대가 돌아가고 그에 맞게 타겟위치가 일시적으로 표시되는 레이더를 구현했으나 기획 목적과 맞지않아
실시간으로 적의 위치를 표시해주는 미니맵 같은 레이더로 변경
먼저 전자의 레이더를 구현해보자
Rader.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Rader : MonoBehaviour
{
public Transform raderCenter;
public RectTransform raderPanel;
public RectTransform sweepLine;
public GameObject blipPrefab;
public RectTransform blipContainer;
public float sweepSpeed = 90f;
public float raderRange = 100f;
public float detectionAngleThreshold = 1.5f;
public float blipLifetime = 1f;
public LayerMask targetLayer;
private float currentAngle = 0f;
private float lastAngle = 0f;
// 감지된 타겟 저장용
private HashSet<Transform> detectedTargets = new HashSet<Transform>();
void Update()
{
DetectTargets();
// 레이더 UI 회전 (플레이어 회전 따라감)
Vector3 euler = raderCenter.eulerAngles;
raderPanel.localRotation = Quaternion.Euler(0, 0, euler.y);
// 회전 막대 회전
lastAngle = currentAngle;
currentAngle += sweepSpeed * Time.deltaTime;
currentAngle %= 360f;
sweepLine.rotation = Quaternion.Euler(0, 0, -currentAngle);
// 한 바퀴 회전 완료 시, 감지 기록 초기화
if (lastAngle > currentAngle) // 360도 넘어간 경우
{
detectedTargets.Clear();
}
}
void DetectTargets()
{
Collider[] targets = Physics.OverlapSphere(raderCenter.position, raderRange, targetLayer);
foreach (var target in targets)
{
if (detectedTargets.Contains(target.transform))
continue;
Vector3 dir = target.transform.position - raderCenter.position;
dir.y = 0;
if (dir.magnitude < 0.01f) continue;
float baseAngle = raderCenter.eulerAngles.y;
float worldAngleToTarget = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg;
float relativeAngle = (worldAngleToTarget - baseAngle + 360f) % 360f;
float deltaAngle = Mathf.DeltaAngle(currentAngle, relativeAngle);
if (Mathf.Abs(deltaAngle) <= detectionAngleThreshold)
{
float radarRadius = raderPanel.rect.width / 2f;
float distancePercent = Mathf.Clamp01(dir.magnitude / raderRange);
float angleRad = currentAngle * Mathf.Deg2Rad;
Vector2 blipPosition = new Vector2(Mathf.Sin(angleRad), Mathf.Cos(angleRad)) * radarRadius * distancePercent;
GameObject blip = Instantiate(blipPrefab, blipContainer);
blip.GetComponent<RectTransform>().anchoredPosition = blipPosition;
Destroy(blip, blipLifetime);
detectedTargets.Add(target.transform);
}
}
}
void OnDrawGizmos()
{
if (raderCenter == null) return;
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(raderCenter.position, raderRange);
}
}
기능
sweepLine이 시계 방향으로 회전하며 타겟 탐지
탐지 범위는 laderRange에 의해 결정
탐지 각도는 detectionAngleThreshold
감지시 해당 위치에 blipPrefab생성
- currentAngle로 회전 상태 관리
- Physics.OverlapSphere()로 범위 내 타겟 검색
- 타겟의 각도를 계산하고 deltaAngle로 회전막대와의 차이 비교
- 스윕 선이 근처를 지나갈 때만 블립 생성
- 한 바퀴 돌면 detectedTargets.Clear()로 detectedTargets의 모든 점들 삭제
public Transform raderCenter;
레이더가 탐지할때 중심이 될 오브젝트 (플레이어)
타겟과 상대 위치/방향 계산 기준
public RectTransform raderPanel;
회전하면서 탐지 역할을 하는 UI 요소 (탐지선)
이 선이 타겟 방향을 지나갈 때 blipPrefab생성
public GameObject blipPrefab;
public RectTransform blipContainer;
타겜 감지했으면 생성되는 점 프리펩
점이 생성될 부모 오브젝트
public float sweepSpeed = 90f;
탐지선의 속도
public float raderRange = 100f;
탐지범위
public float detectionAngleThreshold = 1.5f;
탐지선과 타겟 방향 각도차
blipLifetime
감지된 블립이 몇 초 후 사라지는지 설정.
public LayerMask targetLayer;
타겟의 레이어 지정
private float currentAngle = 0f;
private float lastAngle = 0f;
private HashSet<Transform> detectedTargets = new HashSet<Transform>();
- currentAngle: 탐지선이 현재 몇 도 위치인지.
- lastAngle: 이전 프레임에서의 각도.
- detectedTargets: 이미 감지된 오브젝트를 중복 감지하지 않기 위한 집합
etectTargets() 함수 흐름
Collider[] targets = Physics.OverlapSphere(raderCenter.position, raderRange, targetLayer);
탐지 범위 내에 있는 타겟들을 모두 찾음
foreach (var target in targets)
{
if (detectedTargets.Contains(target.transform))
continue;
이미 감지된 타겟은 중복 감지하지 않음
Vector3 dir = target.transform.position - raderCenter.position;
dir.y = 0;
if (dir.magnitude < 0.01f) continue;
수평면에서의 방향만 고려.
너무 가까운 경우(거리 거의 0)는 무시
각도 계산 및 점 생성
float baseAngle = raderCenter.eulerAngles.y;
float worldAngleToTarget = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg;
float relativeAngle = (worldAngleToTarget - baseAngle + 360f) % 360f;
월드 기준 각도를 플레이어 기준 각도로 변환.
상대 각도(relativeAngle)로 계산
float deltaAngle = Mathf.DeltaAngle(currentAngle, relativeAngle);
현재 탐지선 각도와 타겟이 있는 방향의 각도 차이 계산
if (Mathf.Abs(deltaAngle) <= detectionAngleThreshold)
탑지선 방향 근처에 있으면 점 생성
float radarRadius = raderPanel.rect.width / 2f;
float distancePercent = Mathf.Clamp01(dir.magnitude / raderRange);
블립이 표시 될 거리 비율 계산
float angleRad = currentAngle * Mathf.Deg2Rad;
Vector2 blipPosition = new Vector2(Mathf.Sin(angleRad), Mathf.Cos(angleRad)) * radarRadius * distancePercent;
현재 각도를 기준으로 원형 레이더 위 좌표를 구함
거리 비율을 곱해서 정확한 위치 계산
GameObject blip = Instantiate(blipPrefab, blipContainer);
blip.GetComponent<RectTransform>().anchoredPosition = blipPosition;
Destroy(blip, blipLifetime);
블립 생성 → 지정 시간 후 제거
감지된 타겟은 detectedTargets에 추가
여기까지 Rader.cs
TargetDetectSensor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TargetDetectSensor : MonoBehaviour
{
public Transform raderCenter;
public RectTransform blipContainer;
public RectTransform raderPanel;
public GameObject blipPrefab;
public float raderRange = 100f;
public LayerMask targetLayer;
void Update()
{
DetectTargets();
// 레이더 UI 회전 (플레이어 회전 따라감)
Vector3 euler = raderCenter.eulerAngles;
raderPanel.localRotation = Quaternion.Euler(0, 0, euler.y);
}
void DetectTargets()
{
foreach (Transform child in blipContainer)
{
if (child.CompareTag("Props"))
{
Destroy(child.gameObject);
}
}
Collider[] targets = Physics.OverlapSphere(raderCenter.position, raderRange, targetLayer);
foreach (var target in targets)
{
Vector3 offset = target.transform.position - raderCenter.position;
offset.y = 0f;
float distance = offset.magnitude;
if (distance > raderRange) continue;
float distancePercent = distance / raderRange;
float radarRadius = blipContainer.rect.width / 2f;
Vector2 blipPos = new Vector2(offset.x, offset.z).normalized * radarRadius * distancePercent;
GameObject blip = Instantiate(blipPrefab, blipContainer);
blip.GetComponent<RectTransform>().anchoredPosition = blipPos;
}
}
void OnDrawGizmos()
{
if (raderCenter == null) return;
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(raderCenter.position, raderRange);
}
}
변수
raderCenter | 기준 위치. 일반적으로 플레이어 본체의 Transform |
blipContainer | 블립(UI 아이콘)들을 담는 부모 RectTransform |
raderPanel | 레이더 UI 자체 (회전용) |
blipPrefab | 생성될 블립 오브젝트 프리팹 |
raderRange | 탐지 범위(월드 기준 거리) |
targetLayer | 어떤 레이어에 속한 오브젝트만 감지할지 결정 |
void Update()
{
DetectTargets();
// 레이더 UI 회전 (플레이어 회전 따라감)
Vector3 euler = raderCenter.eulerAngles;
raderPanel.localRotation = Quaternion.Euler(0, 0, euler.y);
}
레이더 ui가 플레이어 y축 회전에 맞춰 회전
DetectTargets()
foreach (Transform child in blipContainer)
{
if (child.CompareTag("Props"))
{
Destroy(child.gameObject);
}
}
이전 프레임에 생성된 블립(Props 태그를 가진 오브젝트) 전부 삭제 → 실시간 갱신.
이 방식은 현재 존재하는 타겟만 항상 최신 상태로 보여줌.
Collider[] targets = Physics.OverlapSphere(raderCenter.position, raderRange, targetLayer);
감지 범위 안의 오브젝트들을 레이어 필터링을 통해 가져옴
Vector3 offset = target.transform.position - raderCenter.position;
offset.y = 0f;
Y축 무시
Vector2 blipPos = new Vector2(offset.x, offset.z).normalized * radarRadius * distancePercent;
플레이어를 중심으로 타겟의 방향과 거리 비율을 이용해 2D UI 상의 위치 계산
미니맵 회전 방향과 연동이 되기 때문에 별도의 회전 보정 필요 없음
공통: OnDrawGizmos()
- 에디터에서 감지 범위 볼수있게 초록색 원 그림
Rader.cs와 TargetDetectSensor.cs비교
항목 Rader.cs (스위핑 방식) TargetDetectSensor.cs (실시간 미니맵 방식)
동작 방식 | 회전 막대(스위프 라인)가 돌며 탐지 | 범위 내의 모든 타겟을 실시간으로 표시 |
블립 생성 조건 | 현재 탐지각도와 타겟 각도가 일치할 때 | 범위 안에 존재하기만 하면 표시 |
UI 연출 | 실제 스캐너처럼 회전하며 감지됨 | 정적인 2D 미니맵 느낌 |
중복 블립 처리 | HashSet을 이용해 한 바퀴 동안 중복 방지 | 매 프레임마다 모두 삭제 후 새로 생성 |
퍼포먼스 | 회전과 탐지각 체크로 약간 무거울 수 있음 | 비교적 단순하지만 매 프레임 블립 재생성은 비용 있음 |
적합한 상황 | SF 스타일 탐지, 감지 재미 요소 강조 | 미니맵 기반 실시간 전략/표시 시스템 |
보완할점: 출구 위치 표시, 좀 더 레이더 같게 blip(점)의 셰이더를 변경 할 필요가 있을듯 레드도트사이트의 레드도트 셰이더처럼 빛이 나고 아웃라인은 좀 흐리게? 하면 더 현실감 있을듯 함