카테고리 없음

언리얼 C++) 언리얼 콜리전 (Collision) 시스템 & 트레이스

eclipse2 2026. 6. 4. 19:41

콜리전은 단순히 눈에 보이는 모델링이 부딪히는 것이 아니라, 내부 질서와 규칙에 따라 작동하는 충돌 처리 전용 시스템.

  • 충돌 반응 규칙: - Block + Block = Block (막힘)
    • Overlap + Overlap (또는 Block) = Overlap (겹침/통과)
    • Ignore + (무엇이든) = Ignore (무시)
  • 오브젝트 채널과 프리셋: 오브젝트 채널은 '내가 어떤 부류(결계, 몬스터 등)인지'를 정의하고, 프리셋은 '내가 앞으로 다른 채널들을 어떻게 대할 것인지(막을지, 겹칠지)'를 미리 세팅해 두는 곳.
  • 단순 vs 복합 콜리전: - 단순 콜리전: 폴리곤 수가 적은 박스, 캡슐 등의 형태. 캐릭터 이동이나 기본 충돌 시 성능을 위해 사용.
    • 복합 콜리전: 스태틱 메시의 실제 형태를 그대로 따라간 정밀한 폴리곤. 정밀 타격이 필요할 때 제한적으로 사용. (Project Settings에서 섞어 쓰기 옵션 설정 가능)
  • 충돌 우선순위: Ignore > Overlap > Block 순으로 강력함. (블록 상태에서는 다른 반응으로 바꿀 수 있지만, 이그노어 상태에서는 블록으로 강제로 바꿀 수 없음)

3. 트레이스 (Trace / Raycast)

보이지 않는 광선을 쏴서 충돌 여부를 검사하는 강력한 무기. 무거운 물리적 총알 객체를 생성하는 대신 트레이스를 쓰면 멀티플레이 서버 부하(최적화)를 크게 줄일 수 있음.

  • Kismet vs UWorld 트레이스: - Kismet: UKismetSystemLibrary를 사용. 디버깅 선 긋기(DrawDebug)와 매개변수 정리가 잘 되어 있어 개발 시 매우 편리함.
    • UWorld: GetWorld()를 직접 사용. 디버그는 불편하지만, 자유도가 높고 미세하게 속도가 더 빠름.
  • Single vs Multi 트레이스:
    • Single Trace: 광선을 쏘다가 처음으로 Block 당한 물체 딱 하나만 반환하고 멈춤.
    • Multi Trace: 광선을 쏘며 관통하는 여러 물체들을 배열로 담아 반환함. (Overlap 판정까지 모두 인식 가능)
  • 비동기 트레이스 (Async Trace):
    • 한 번에 수많은 광선을 쏠 때(예: AI 정찰 시스템) 게임이 끊기는(동기 방식의 한계) 것을 막기 위해, 별도의 작업대(스레드)에서 뒷단 계산을 시키는 방식.
    • 트레이스 작업이 끝나면 델리게이트(OnAsyncTraceCompleted)를 통해 순차적으로 결과물(OutHits)을 돌려받아 안전하게 처리함.

예시 코드:

① 동기 트레이스 (Single)

한 번에 하나의 물체만 블록(Block) 판정으로 찾아냄 

void ATraceTest::StartSingleTrace()
{
    FHitResult HitResult;
    TArray<AActor*> ActorsToIgnore;
    ActorsToIgnore.Add(this); // 나 자신(Player)이 맞는 것 방지

    UKismetSystemLibrary::LineTraceSingle(
        GetWorld(),
        GetActorLocation(), // 시작점
        GetActorForwardVector() * 1000.f + GetActorLocation(), // 끝점 (앞으로 1000)
        UEngineTypes::ConvertToTraceType(ECC_Visibility), // 충돌 채널
        false, ActorsToIgnore, 
        EDrawDebugTrace::ForOneFrame, // 디버그 선 표시
        HitResult, true, FLinearColor::Red, FLinearColor::Green, 5.0f
    );
}

 

② 비동기 트레이스 (Async Multi)

여러 물체를 관통(Overlap)하여 찾거나, 한 번에 수많은 트레이스를 쏠 때 메인 스레드 부하를 줄이기 위해 사용

void ATraceTest::StartAsyncTrace()
{
    // 1. 작업이 끝나면 돌려받을 함수(델리게이트) 연결
    FTraceDelegate TraceDelegate;
    TraceDelegate.BindUObject(this, &ATraceTest::OnAsyncTraceCompleted);

    FCollisionQueryParams QueryParams;
    QueryParams.AddIgnoredActor(this);

    // 2. 비동기 요청 (뒷단에서 연산 시작)
    AsyncTraceHandle = GetWorld()->AsyncLineTraceByChannel(
        EAsyncTraceType::Multi, 
        GetActorLocation(), GetActorForwardVector() * 1000.f + GetActorLocation(),
        ECC_Visibility, QueryParams, FCollisionResponseParams::DefaultResponseParam, &TraceDelegate
    );
}

// 3. 작업 완료 후 실행되는 콜백 함수
void ATraceTest::OnAsyncTraceCompleted(const FTraceHandle& Handle, FTraceDatum& Data)
{
    for (const FHitResult& Hit : Data.OutHits)
    {
        AActor* HitActor = Hit.GetActor();
        UE_LOG(LogTemp, Warning, TEXT("Hit: %s"), *HitActor->GetName());
    }
}

4. 데미지 시스템 (Damage System)

언리얼 엔진에 내장된 유기적인 데미지 전달 아키텍처.

  • 데미지 주고받기: - 송신 (가해자): ApplyDamage(일반), ApplyPointDamage(총기/방향성), ApplyRadialDamage(폭발) 함수 사용.
    • 수신 (피해자): TakeDamage 가상 함수를 오버라이딩하여 체력 감소 로직 구현.
  • UDamageType의 역할: 이 데미지가 물리 타격인지, 화상인지, 낙사 등 월드 데미지인지 '성격'을 규정해 주는 기준표.
  • CDO(원본 객체) 활용의 정석: 데미지 타입을 확인할 때 매번 새 객체를 만들지 않고, CDO 원본 마스터 객체 하나를 슥 훑어봄. DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>() 형태로 가져와서 매우 가볍고 빠르게 특수 로직(예: 화상 지속 시간, 방어력 관통 등)을 적용할 수 있음.

① 가해자 측: 데미지 입히기 (ApplyDamage)

// OnAsyncTraceCompleted 같은 함수 내부에서 적중한 액터에게 데미지 전달
UGameplayStatics::ApplyPointDamage(
    HitActor,                            // 피격 대상 (맞은 놈)
    50.f,                                // 데미지 수치
    GetActorForwardVector(),             // 공격 방향 (밀려나는 방향 계산용)
    Hit,                                 // 히트 정보 (맞은 부위 등)
    GetInstigatorController(),           // 가해자 컨트롤러
    this,                                // 유발 액터 (나)
    UMyTestDamageType::StaticClass()     // 데미지 성격 (불, 독 등)
);

② 피해자 측: 데미지 받기 (TakeDamage 오버라이드)

float AMyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float FinalDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

    // 전달받은 데미지 타입의 CDO(원본 객체)를 가져옴!
    const UFireDamageType* FireDamage = DamageEvent.DamageTypeClass->GetDefaultObject<UFireDamageType>();
    
    if (FireDamage)
    {
        // 불 데미지 전용 특수 로직 처리
        FinalDamage *= (1.0f + FireDamage->ArmorPenetration); // 방어력 관통 적용
        this->ApplyBurnEffect(FireDamage->BurnDuration);      // 화상 지속시간 적용
    }

    CurrentHP -= FinalDamage;
    return FinalDamage;
}