AI 봇으로 인게임 보이지 않는 벽 충돌 누락을 전수 탐색하는 방법

Unity 2022.3 LTS에서 AI 봇과 캡슐 프로브를 결합해 보이지 않는 벽의 충돌 누락 후보를 탐색하는 방법을 설명합니다. 기대 경계, 탐색 커버리지, 실제 이동 결과와 리플레이 기록을 사용하는 QA 설계안을 정리합니다.

✓ 복사 완료!
챕터자동화 테스트 & 회귀 검증
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
AI 봇으로 인게임 보이지 않는 벽 충돌 누락을 전수 탐색하는 방법

핵심 요약

이 설계안은 Unity 2022.3 LTS와 C# 9 기준으로 기대 경계 데이터를 별도로 만들고 CapsuleCast 기반 물리 프로브와 AI 봇을 결합해 충돌이 있어야 하는 구간과 실제 차단 결과의 불일치 후보를 수집합니다. 고정 시드와 물리 틱은 재현 조건의 일부이며, 물리 엔진의 플랫폼 간 결정성을 보장하는 장치는 아닙니다.

단순한 무작위 이동은 전수 검사가 아닙니다. 그리드 또는 Poisson disk 샘플링, 양방향 경계 추적, 높이와 캡슐 크기별 테스트, 고정 시드 리플레이를 함께 사용해야 재현 가능한 탐색 범위를 만들 수 있습니다.

보이지 않는 벽 충돌 누락은 무엇을 의미하는가?

보이지 않는 벽은 플레이어가 접근해서는 안 되는 영역을 물리적으로 차단하는 경계입니다. 렌더러에는 표시되지 않지만 다음과 같은 방식으로 구현될 수 있습니다.

  • 별도의 BoxCollider, CapsuleCollider, MeshCollider
  • 충돌 전용 레이어와 Physics Layer Collision Matrix
  • NavMesh의 Not Walkable 영역을 통한 경로 제한. 이는 물리 벽을 생성하는 기능과는 다릅니다.
  • 트리거 볼륨과 별도의 이동 제한 로직
  • 스트리밍 씬이 로드될 때 생성되는 런타임 콜라이더

QA에서 말하는 충돌 누락은 보통 다음 조건을 뜻합니다.

구분기대 상태실제 상태판정
벽 누락봇이 통과하면 안 됨물리 충돌 없음충돌 누락 후보
오탐 충돌통과 가능해야 함물리 충돌 발생이동 회귀 또는 과잉 차단
NavMesh 불일치이동 불가 영역NavMeshAgent가 우회하지 않음경로 데이터 검증 필요
터널링충돌해야 함고속 이동에서 통과연속 충돌 검토 필요

핵심은 현재 위치만 확인하는 것이 아니라 봇의 이동 선분과 기대 경계가 교차하는지 확인하는 것입니다. NavMeshAgent가 경로를 피했다고 해서 월드 콜라이더가 정상이라는 뜻은 아닙니다. 따라서 경로 탐색 검증과 물리 충돌 검증은 분리해야 합니다.

왜 사람이 직접 플레이하거나 무작위 봇을 사용하면 놓치는가?

방향이나 높이별 결함을 검사하려면 해당 조건을 테스트에 포함해야 합니다. 다음 표는 테스트별로 확인할 조건과 누락될 수 있는 시나리오의 예시이며, 결함 발생 빈도를 조사한 결과는 아닙니다.

테스트 방식잘 찾는 문제놓치기 쉬운 문제
수동 플레이주요 동선의 명백한 통과맵 외곽, 낮은 턱, 역방향 진입
NavMesh 경로 테스트워크 가능 영역 오류실제 콜라이더 누락, 레이어 불일치
무작위 이동우연히 발생하는 넓은 구역의 결함좁은 경계, 재현이 어려운 방향성 문제
단일 속도 봇기본 캡슐 크기의 충돌대시, 점프, 루트 모션, 큰 캐릭터
레이캐스트 하나얇은 벽의 정면 충돌캐릭터 몸통과 모서리의 캡슐 충돌

특히 단일 레이캐스트는 캐릭터의 반지름을 고려하지 않기 때문에 벽 모서리를 비껴가거나 몸통이 벽 안으로 들어가는 문제를 검출하지 못할 수 있습니다. 플레이어와 NPC의 실제 충돌 형상이 캡슐이라면 테스트 프로브도 캡슐이어야 합니다.

AI 봇 기반 전수 탐색의 핵심 구조는 무엇인가?

이 설계안에서는 자동 탐색기를 다음 네 가지 구성요소로 나눕니다.

  1. Expected Boundary: 차단되어야 하는 경계를 정의한 기준 데이터
  2. Explorer Bot: 경계와 미탐색 구역을 따라 움직이는 봇
  3. Collision Oracle: 기대 결과와 실제 물리 결과를 비교하는 판정기
  4. Replay Recorder: 동일한 시드와 물리 틱으로 문제를 재현하는 기록기

보이지 않는 벽 기대 충돌 영역과 AI 봇 탐색 경로를 겹쳐 표시한 디버그 화면

이 구조를 사용하면 AI 봇의 행동이 복잡해져도 버그 판정 기준은 변하지 않습니다. 봇은 탐색을 담당하고 오라클은 결과를 판정합니다.

기대 경계 데이터는 어떻게 설계해야 하는가?

실제 충돌 콜라이더를 기대 데이터로 그대로 사용하면 누락을 비교할 기준이 사라집니다. 따라서 아티스트나 레벨 디자이너가 배치한 별도의 검증용 볼륨 또는 경계 선분을 사용합니다.

권장 필드는 다음과 같습니다.

필드용도
boundaryId버그 리포트와 리플레이를 연결하는 식별자
sceneName씬 스트리밍 오류 추적
region던전, 필드, 보스룸 등 기능 영역
expectedNormal진입 금지 방향 검증
minHeight, maxHeight지면과 공중 경계 분리
allowedCapsuleRadius플레이어와 NPC 크기별 예외
dynamicCondition문 열림, 퀘스트 상태 등 조건부 경계
severity출시 차단 여부와 우선순위

기대 경계는 ExpectedInvisibleWall 레이어의 프리팹이나 ScriptableObject로 관리할 수 있습니다. 중요한 점은 이 레이어가 실제 물리 차단 레이어와 달라야 한다는 것입니다. 같은 콜라이더를 양쪽 기준으로 사용하면 누락을 발견할 수 없습니다.

AI 봇은 어떤 경로로 움직여야 하는가?

1. 공간 샘플링으로 기본 커버리지를 만든다

맵을 일정 간격의 그리드로 나누거나 Poisson disk sampling으로 최소 거리를 보장하는 목표 지점을 생성합니다. 지점마다 다음 정보를 저장합니다.

  • 월드 좌표
  • 지면 높이와 경사도
  • 주변 기대 벽의 boundaryId
  • 접근 가능한 방향 벡터
  • 현재까지 방문한 횟수

그리드 간격은 캐릭터 크기와 검출하려는 최소 결함 크기에 맞춰 정합니다. 반지름보다 작은 간격을 택하더라도 모든 좁은 누락 구간을 검출한다는 보장은 없습니다. 유한한 샘플만으로 모든 연속 공간을 수학적으로 완전히 증명할 수 없으므로 보고서에는 간격과 적용한 커버리지 기준을 함께 기록해야 합니다.

2. 경계 추적과 양방향 진입을 사용한다

봇이 목표 지점만 향하게 하면 벽의 정면은 검사해도 모서리나 평행 이동 구간은 건너뛸 수 있습니다. 다음 행동을 조합하는 방식이 실용적입니다.

  • 목표 지점 이동
  • 기대 경계에서 일정 거리를 유지하는 wall-following
  • 경계의 법선 방향으로 진입하는 probe
  • 경계의 접선 방향으로 이동하는 scrape test
  • 같은 경계를 반대 방향으로 다시 통과하는 reverse test

각 경계에는 접선 방향 T와 진입 방향 N을 정의합니다. 봇은 N, -N, T, -T 방향을 최소 한 번씩 검사해야 방향성에 따른 레이어 또는 이동 코드 오류를 찾을 수 있습니다.

3. 봇의 물리 조건을 여러 조합으로 실행한다

한 종류의 봇만 사용하면 캐릭터 크기와 속도에 따른 문제를 놓칩니다. 최소한 다음 조합을 분리해 실행합니다.

테스트 프로필주요 검증 대상
보행 봇일반적인 캡슐 충돌
대형 NPC 봇벽과 모서리의 반지름 처리
고속 이동 봇Discrete 충돌과 터널링
점프 봇공중 경계와 높이 제한
루트 모션 봇애니메이션 이동과 물리 이동 불일치
네트워크 보정 봇위치 보정과 순간 이동 예외

대시나 순간 이동은 일반적인 CapsuleCast만으로 동일하게 재현되지 않을 수 있습니다. 해당 기능은 실제 이동 코드와 동일한 입력 경로로 별도 시나리오를 실행해야 합니다.

충돌 오라클은 어떻게 구현하는가?

충돌 오라클은 기대 상태와 실제 상태를 분리해서 읽어야 합니다. 아래 예시는 Unity 2022.3 LTS, C# 9 기준의 단순한 캡슐 프로브입니다. expectedWallMask는 검증용 기대 경계이고 physicalCollisionMask는 실제 게임 충돌 레이어입니다. 시작 캡슐이 두 레이어의 콜라이더와 겹치지 않는 안전 지점에서 호출합니다. CapsuleCast는 시작부터 겹친 콜라이더를 검출하지 않으므로 그런 샘플은 별도의 겹침 검사로 분류해야 합니다.

using UnityEngine;

public readonly struct ProbeResult
{
    public readonly bool expected;
    public readonly bool blocked;
    public readonly float hitDistance;

    public ProbeResult(bool expected, bool blocked, float hitDistance)
    {
        this.expected = expected;
        this.blocked = blocked;
        this.hitDistance = hitDistance;
    }

    public static ProbeResult Invalid => new(false, false, -1f);
}

public sealed class InvisibleWallProbe : MonoBehaviour
{
    [SerializeField] private LayerMask expectedWallMask;
    [SerializeField] private LayerMask physicalCollisionMask;
    [SerializeField] private float radius = 0.35f;
    [SerializeField] private float height = 1.8f;
    [SerializeField] private float castDistance = 0.8f;

    public ProbeResult Check(Vector3 position, Vector3 direction)
    {
        direction.y = 0f;
        if (direction.sqrMagnitude < 0.0001f)
            return ProbeResult.Invalid;

        direction.Normalize();
        Vector3 bottom = position + Vector3.up * radius;
        Vector3 top = position + Vector3.up * (height - radius);

        bool expected = Physics.CapsuleCast(
            bottom,
            top,
            radius,
            direction,
            out RaycastHit expectedHit,
            castDistance,
            expectedWallMask,
            QueryTriggerInteraction.Collide);

        bool blocked = Physics.CapsuleCast(
            bottom,
            top,
            radius,
            direction,
            out RaycastHit hit,
            castDistance,
            physicalCollisionMask,
            QueryTriggerInteraction.Ignore);

        // 기대 경계 이후에야 막혔다면 해당 경계를 정상 차단했다고 볼 수 없다.
        const float boundaryTolerance = 0.01f; // 프로젝트 월드 단위의 실험값
        bool blockedAtExpectedBoundary = blocked
            && (!expected || hit.distance <= expectedHit.distance + boundaryTolerance);
        return new ProbeResult(expected, blockedAtExpectedBoundary, blocked ? hit.distance : -1f);
    }
}

이 예제는 캡슐 높이가 반지름의 두 배 이상이고 반지름과 검사 거리가 양수이며, position이 똑바로 선 캡슐의 바닥 위치라고 가정합니다. 임의의 회전과 스케일은 반영하지 않습니다. 기대 경계보다 뒤에 있는 다른 콜라이더가 검출됐다는 이유만으로 해당 경계가 차단됐다고 판정하지 않도록 거리를 비교합니다. 반대로 경계 앞의 무관한 장애물 때문에 이동이 막힌 경우에는 대상 경계를 검사한 것이 아니므로 추가 구현에서 콜라이더·경계 ID를 기록하고 미검사 또는 CONDITIONAL로 분류해야 합니다. 이 축약 구조의 불리언만으로 경계별 최종 합격을 결정하지 않습니다.

안전 지점에서 금지 영역으로 프로브를 이동시켜 기대 경계와 실제 차단의 위치를 비교하고, 누락 후보에는 실제 이동 기능을 사용한 재현 검사를 이어갑니다. 쿼리는 레이어 마스크와 트리거 설정을 사용하지만 플레이어 Rigidbody의 모든 충돌 설정과 이동 로직을 실행하는 것은 아닙니다.

다음 항목도 함께 기록해야 원인 분석이 빨라집니다.

  • Physics.defaultSolverIterations와 고정 물리 틱
  • 봇의 위치, 회전, 속도, 캡슐 반지름과 높이
  • LayerMask 값과 Physics Layer Collision Matrix 상태
  • QueryTriggerInteraction 설정
  • 충돌 콜라이더의 인스턴스 식별자
  • 씬 로드 상태와 스트리밍 셀 이름
  • Rigidbody.collisionDetectionMode
  • CharacterController.Move 또는 실제 이동 함수의 호출 결과

AI 봇 충돌 누락 탐색은 어떤 순서로 구현하는가?

Step 1. 검증용 경계를 배치한다

레벨의 모든 보이지 않는 벽에 ExpectedInvisibleWall 볼륨을 연결합니다. 문이나 퀘스트 영역처럼 상태에 따라 통과가 바뀌는 경계는 조건식을 함께 저장합니다.

예를 들어 문이 닫힌 상태에서는 expected = true이고 문이 열린 상태에서는 expected = false가 되도록 테스트 상태를 명시합니다. 이 조건이 없으면 정상적인 동적 통과도 버그로 보고됩니다.

Step 2. 탐색 목표와 접근 방향을 생성한다

경계 선분을 일정 간격으로 샘플링한 뒤 각 샘플에 대해 안전 측 위치와 금지 측 위치를 만듭니다. 각 샘플은 다음 테스트를 가집니다.

  1. 안전 측에서 금지 측으로 이동
  2. 금지 측에서 안전 측으로 이동
  3. 경계에 평행하게 이동
  4. 경계 모서리를 대각선으로 통과
  5. 캐릭터 높이와 반지름을 바꿔 반복

Step 3. 고정 틱으로 봇을 실행한다

테스트 빌드에서는 Time.fixedDeltaTime을 고정하고 물리 틱마다 입력과 결과를 기록합니다. 탐색 봇의 의사 난수 생성기는 시드로 초기화해야 합니다.

같은 씬 빌드, 시드, 입력 시퀀스뿐 아니라 플랫폼, 물리 설정, 객체 생성·호출 순서, 시작 상태도 통제해 재현 여부를 확인합니다. 고정 틱과 난수 시드만으로 PhysX의 모든 결과가 플랫폼 간 동일하다고 보장할 수는 없습니다. 렌더링 프레임 수에 따라 판단하는 코루틴은 렌더링 성능에 영향을 받으므로 물리 검증은 고정 틱 기준으로 처리하는 편이 안전합니다.

Step 4. 실패를 자동 분류한다

판정 결과는 다음 네 가지로 분류할 수 있습니다.

결과조건처리
PASS기대 경계와 실제 차단 결과가 일치통과
MISSING_COLLIDER차단 기대, 물리 차단 없음높은 우선순위 버그
UNEXPECTED_BLOCK통과 기대, 물리 충돌 있음이동 회귀 후보
CONDITIONAL상태나 스트리밍 조건 확인 필요재실행 큐 등록

실패 샘플에는 재실행 결과와 지속 시간을 함께 기록합니다. 연속 두 틱 이상 실패했는지 확인하는 것은 분류 기준의 예시이며 오탐 제거의 증명은 아닙니다. 한 틱만 발생한 터널링도 실제 결함일 수 있으므로 원본 실패를 버리지 않고 실제 이동 경로와 함께 검토합니다.

false positive를 어떻게 줄이는가?

자동 충돌 검사는 정상적인 예외를 자주 버그로 보고할 수 있습니다. 다음 예외는 기대 경계 데이터에 명시적으로 포함해야 합니다.

  • 문이 열리는 애니메이션 구간
  • 이동 플랫폼과 엘리베이터
  • 임시로 비활성화되는 방벽
  • 특정 팀이나 퀘스트 상태에서만 통과하는 길
  • 씬 스트리밍 중 충돌 데이터가 아직 로드되지 않은 구간
  • 텔레포트처럼 연속 이동이 아닌 기능

또한 CheckCapsule의 결과와 실제 이동 결과를 혼동하지 않아야 합니다. CharacterController는 콜라이더에 접촉해도 슬라이딩이나 step offset을 적용할 수 있습니다. 따라서 다음 세 값을 각각 기록하는 것이 좋습니다.

  1. 물리 프로브가 예상한 차단 여부
  2. 실제 이동 함수가 반환한 충돌 플래그
  3. 봇의 최종 위치가 경계를 넘어갔는지 여부

세 값 중 하나만 보면 슬라이딩, 계단 오르기, 경사면 처리 때문에 잘못된 결론을 내릴 수 있습니다.

빌드와 패치 검증에 어떻게 연결하는가?

빌드별 회귀를 비교하려면 테스트 빌드와 실행 조건을 연결합니다. 이 설계안의 보고서에는 다음 정보를 포함합니다.

  • 빌드 번호와 커밋 해시
  • 씬 이름과 씬 번들 또는 어드레서블 버전
  • 탐색 시드와 봇 프로필
  • 실패한 boundaryId
  • 실패한 고정 틱 번호
  • 시작 위치와 종료 위치
  • 봇의 속도, 캡슐 크기, 이동 모드
  • 기대 레이어와 실제 충돌 레이어
  • 재현용 입력 시퀀스

패치 검증에서는 전체 맵을 매번 실행하는 회귀 스위트와 변경된 씬 주변만 실행하는 빠른 스위트를 나누면 됩니다.

  • 프리 머지 검사: 변경된 씬의 경계와 인접 셀
  • 나이틀리 검사: 모든 씬과 모든 봇 프로필
  • 릴리스 후보 검사: 전체 탐색과 실패 리플레이 재실행
  • 핫픽스 검사: 수정된 boundaryId와 동일한 물리 조건

실패 리플레이는 버그를 재현하는 최소 입력으로 저장해야 합니다. 긴 전체 탐색 로그만 남기면 개발자가 문제 지점을 찾기 어렵습니다. 최초 실패 전후의 약 30~60개 고정 틱과 관련 상태만 보존하는 방식이 실용적입니다.

자주 묻는 질문 FAQ

찾을 수 없습니다. NavMesh는 이동 가능한 표면과 경로 데이터를 검증하는 도구이고 실제 물리 콜라이더의 존재와 레이어 충돌을 보장하지 않습니다. NavMesh 검증과 CapsuleCast 기반 물리 검증을 함께 사용해야 합니다.

무작위 AI 봇을 오래 실행하면 전수 검사가 되나요?

아닙니다. 무작위 실행은 탐색 확률을 높일 뿐 전체 구간 검사를 증명하지 않습니다. 경계 샘플 수, 접근 방향, 높이 레이어, 봇 프로필, 실행 시드를 정의하고 커버리지 수치를 보고해야 합니다.

Raycast 대신 CapsuleCast를 사용해야 하는 이유는 무엇인가요?

플레이어와 NPC는 점이 아니라 부피를 가진 객체이기 때문입니다. CapsuleCast는 이동 캡슐의 반지름과 높이를 반영해 벽 모서리, 좁은 틈, 몸통 관통 문제를 레이캐스트보다 실제 플레이 조건에 가깝게 검증합니다.

정리

AI 봇을 활용한 보이지 않는 벽 전수 탐색의 핵심은 봇의 지능보다 명확한 기대 경계와 결정적인 판정 기준입니다. 검증용 경계를 실제 콜라이더와 분리하고 그리드 샘플링과 양방향 wall-following으로 접근 범위를 만들며 CapsuleCast와 실제 이동 결과를 함께 기록하면 충돌 누락을 재현 가능한 자동 QA 이슈로 바꿀 수 있습니다.

단, 유한한 샘플만으로 모든 연속 공간을 완전히 증명할 수는 없습니다. 따라서 자동화 테스트의 결과는 전수 완료라는 표현보다 어떤 경계와 방향, 캐릭터 프로필을 얼마나 검사했는지 보여주는 커버리지와 실패 리플레이로 관리하는 것이 정확합니다.