상호작용 UI 시스템 (Timer)

김여울·2025년 11월 5일
0

내일배움캠프

목록 보기
108/114

목적

  • 플레이어가 상호작용 가능한 액터를 볼 때, HUD에 “E”키 아이콘을 화면 상에 표시해주는 UI 시스템
  • Timer 기반으로 성능을 최적화하고, 액터의 3D 위치를 2D로 변환해서 HUD에 그리기

주요 기능

  • 실시간 Trace : 플레이어 카메라에서 Ray를 발사해 상호작용 가능한 액터를 감지
  • 동적 UI 위치 : 객체의 3D 월드 좌표를 2D 화면 좌표로 변환해 정확한 위치에 UI 표시
  • 성능 최적화 : Tick 대신 Timer 기반으로 필요한 주기마다 업데이트
  • 인터페이스 : IInteractable 인터페이스로 확장성 제공

시스템 구성도

┌─────────────────────────────────────────────────────────────┐
│                    CharacterBase_GAS                        │
│              (플레이어 캐릭터 - 부모 클래스)                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────┐     ┌──────────────────┐              │
│  │InteractionComponent│    │  CameraComponent │             │
│  │  (20Hz Timer)      │    │  (좌표 기준점)    │             │
│  │  - Trace Actor    │    │                  │              │
│  │  - FocusedActor   │    │                  │              │
│  └──────────────────┘     └──────────────────┘              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
          ↓ FocusedActor 정보 전달
          ↓ (GetFocusedActor)
┌─────────────────────────────────────────────────────────────┐
│                     MyProjectHUD                            │
│              (HUD UI 렌더링 - 10Hz Timer)                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────┐     ┌──────────────────┐              │
│  │UpdateInteractionUI│    │     DrawHUD      │              │
│  │  (0.1초 주기)      │    │  (매 프레임)      │             │
│  │  - WorldToScreen  │    │  - E 키 렌더링   │               │
│  │  - 위치 변환      │    │  - 2D 그리기     │               │
│  └──────────────────┘     └──────────────────┘              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
          ↓ 상호작용 인터페이스 조회
          ↓ (Implements<IInteractable>)
┌─────────────────────────────────────────────────────────────┐
│               PuzzleActor (상호작용 객체)                    │
│          (IInteractable, IPickupable 구현)                  │
├─────────────────────────────────────────────────────────────┤
│  - Interact(): 상호작용 로직                                 │
│  - GetUIWorldLocation(): UI 표시 위치                        │
│  - BeginHighlight(): 강조 표시 시작                          │
│  - EndHighlight(): 강조 표시 종료                            │
└─────────────────────────────────────────────────────────────┘

구현 요소

1️⃣ InteractionComponent (상호작용 감지)

역할 : 플레이어 주변의 상호작용 가능한 액터를 주기적으로 감지

속성 :

  • TraceInterval: 0.05초 (20Hz) - Trace 실행 주기
  • InteractionDistance: 1000.0 단위 - 감지 거리
  • FocusedActor: 현재 감지된 상호작용 객체

핵심 함수 :

void BeginPlay()
{
	// Timer 시작 - 0.05초마다 UpdateTrace 호출
	GetWorld()->GetTimerManager().SetTimer(
        TraceTimerHandle,
        this,
        &UInteractionComponent::UpdateTrace,
        TraceInterval,  // 0.05초
        true            // 반복
    );
}

void UpdateTrace()
{
	// Trace 실행 및 FocusedActor 업데이트
	FHitResult HitResult;
	if (TraceForInteractable(HitResult))
	{
		// Interactable 인터페이스를 구현한 객체 감지
		FocusedActor = HitResult.GetActor();
	}
}

bool TraceForInteractable(FHitResult& HitResult)
{
	// 카메라에서 Ray 발사 (앞으로 1000 단위)
	// Interactable 인터페이스 구현 여부 확인
}

최적화 :

  • Tick 사용 안 함 (매 프레임 60Hz 실행)
  • Timer 사용 (20Hz만 실행) → 약 66% 성능 개선

2️⃣ MyProjectHUD (UI 위치 계산 및 렌더링)

역할: 월드 좌표를 화면 좌표로 변환하고 UI 렌더링

구성:

  • UpdateInteractionUI() - 위치 계산 (Timer: 0.1초)
  • DrawHUD() - UI 렌더링 (매 프레임)

1-1. 월드 좌표 → 화면 좌표 변환

void AMyProjectHUD::UpdateInteractionUI()
{
    // 1단계: 플레이어 캐릭터 가져오기
    ACharacterBase_GAS* PlayerCharacter = Cast<ACharacterBase_GAS>(
        UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)
    );

    // 2단계: 상호작용 컴포넌트 가져오기
    UInteractionComponent* InteractionComp = 
        PlayerCharacter->FindComponentByClass<UInteractionComponent>();

    // 3단계: 감지된 액터 가져오기
    AActor* FocusedActor = InteractionComp->GetFocusedActor();

    // 4단계: 인터페이스에서 월드 위치 조회
    IInteractable* Interactable = Cast<IInteractable>(FocusedActor);
    FVector WorldLocation = Interactable->GetUIWorldLocation();  // 3D 좌표

    // 5단계: 화면 좌표로 변환 (ProjectWorldToScreen)
    FVector2D ScreenPosition;
    APlayerController* PC = GetOwningPlayerController();
    bool bIsOnScreen = PC->ProjectWorldLocationToScreen(
        WorldLocation,      // 입력: 3D 월드 좌표
        ScreenPosition      // 출력: 2D 화면 좌표 (0~1920, 0~1080)
    );

    // 6단계: 오프셋 적용 및 캐시에 저장
    CachedScreenPosition = ScreenPosition + UIOffset;  // 위치 조정
    bShouldShowUI = true;  // 그리기 허용 플래그
}

1-2. 2D 변환

3D 월드 좌표 (X, Y, Z)[ProjectWorldToScreen]
2D 화면 좌표 (ScreenX, ScreenY)

예시:
- 월드 좌표: (750, -30, 124.5)
- 화면 좌표: (425.28, 271.10)  ← 화면의 정확한 위치!

2. 화면에 UI 렌더링

void AMyProjectHUD::DrawHUD()
{
    Super::DrawHUD();

    // 조건 확인: UI를 그려야해?
    if (!bShouldShowUI)
        return;

    // 텍스처 또는 텍스트로 UI 그리기
    if (InteractionTexture)
    {
        // 텍스처 기반 렌더링 (권장)
        FVector2D DrawPosition = CachedScreenPosition - (UISize * 0.5f);

        DrawTexture(
            InteractionTexture,           // E 키 아이콘 텍스처
            DrawPosition.X,               // 화면 X 좌표
            DrawPosition.Y,               // 화면 Y 좌표
            UISize.X,                     // 너비 (픽셀)
            UISize.Y,                     // 높이 (픽셀)
            0.0f, 0.0f, 1.0f, 1.0f,     // UV 좌표 (전체 표시)
            FLinearColor::White           // 색상 (흰색)
        );
    }
    else
    {
        // 텍스트 기반 렌더링 (폴백)
        DrawText(
            TEXT("E"),
            FLinearColor::White,
            CachedScreenPosition.X,
            CachedScreenPosition.Y,
            GEngine->GetLargeFont(),
            2.0f  // 크기
        );
    }
}

3️⃣ 인터페이스 패턴 (IInteractable)

역할: 상호작용 가능한 객체들이 따라야 할 계약 정의

// Interactable.h
UINTERFACE(MinimalAPI)
class UInteractable : public UInterface
{
    GENERATED_BODY()
};

class MYPROJECT_API IInteractable
{
    GENERATED_BODY()

public:
    // 필수 구현 함수
    virtual void Interact(UInteractionComponent* Interactor) = 0;

    // 선택적 구현 함수 (기본 구현 제공)
    virtual void BeginHighlight() {}      // 강조 시작
    virtual void EndHighlight() {}        // 강조 종료

    // HUD 위치 반환 (중요!)
    virtual FVector GetUIWorldLocation() const { return FVector::ZeroVector; }
};

PuzzleActor 구현

class APuzzleActor : public AActor, public IInteractable
{
public:
    // 상호작용 로직
    virtual void Interact(UInteractionComponent* Interactor) override
    {
        // 실제 상호작용 처리
        Solve();
    }

    // UI 표시 위치 (예: 액터 위쪽)
    virtual FVector GetUIWorldLocation() const override
    {
        return GetActorLocation() + FVector(0, 0, 100);
    }
};

블루프린트에서 설정

GameMode에서 HUD 클래스 설정

BP_GASGameMode 

  • Details 패널 열기
  • HUD Class → MyProjectHUD 선택

텍스처 설정

BP_MyProjectHUD 블루프린트 생성:

  1. Parent Class: MyProjectHUD
  2. Details → UI|Interaction:
    • Interaction Texture: "E" 키 이미지 선택
    • UI Size: (50, 50) 또는 원하는 크기
    • UI Offset: (0, -20) 또는 원하는 오프셋
    • Update Interval: 0.1 (기본값)

GameMode에서

  • HUD Class → BP_MyProjectHUD 선택

동작 흐름

전체 시퀀스

[0.0s] 게임 시작
  │
  ├─→ CharacterBase_GAS BeginPlay
  │   └─→ InteractionComponent BeginPlay
  │       └─→ Timer 시작 (0.05초 주기)
  │
  ├─→ MyProjectHUD BeginPlay
  │   └─→ Timer 시작 (0.1초 주기)[0.05s] InteractionComponent Timer 콜백 #1
  │
  ├─→ UpdateTrace() 실행
  │   ├─→ LineTraceSingleByChannel 실행
  │   ├─→ HitResult 분석
  │   └─→ FocusedActor = PuzzleActor_C_0 (감지!)[0.1s] MyProjectHUD Timer 콜백 #1
  │
  ├─→ UpdateInteractionUI() 실행
  │   ├─→ InteractionComponent->GetFocusedActor()
  │   │   └─→ PuzzleActor_C_0 반환
  │   │
  │   ├─→ IInteractable->GetUIWorldLocation()
  │   │   └─→ (750, -30, 124.5) 반환
  │   │
  │   ├─→ PC->ProjectWorldLocationToScreen()
  │   │   ├─ 입력: (750, -30, 124.5)
  │   │   └─ 출력: (425.28, 271.10) ← 화면 좌표!
  │   │
  │   └─→ CachedScreenPosition = (425.28, 251.10) + UIOffset
  │
[0.1s] DrawHUD() - 매 프레임 실행
  │
  ├─→ bShouldShowUI 확인
  │   └─→ true (UI 표시)
  │
  ├─→ DrawTexture(
  │       InteractionTexture,425.28,      ← X 좌표 (화면 중앙 근처)251.10,      ← Y 좌표 (약간 위)50, 50,      ← 크기 (50x50 픽셀)...)
  │
  └─→ 화면에 "E" 키 아이콘 표시!

성능 최적화

항목Tick 방식Timer 방식 (현재)
호출 빈도60Hz (매 프레임)20Hz (0.05초)
프로세스60회/초20회/초
성능 절약-66% 감소
반응성즉시50ms 지연 (거의 안 느껴짐)

타이머 전략

// InteractionComponent: 빠른 감지 (20Hz)
// - Trace는 계산 비용이 큼
// - 0.05초 = 50ms (반응성 우수)
TraceInterval = 0.05f;  // 20Hz

// HUD: 느린 렌더링 (10Hz)
// - 위치 계산은 가벼움
// - 0.1초 = 100ms (DrawHUD가 매 프레임 어차피 실행)
UpdateInterval = 0.1f;  // 10Hz

데이터 흐름

[World Coordinate]
      ↓
(750, -30, 124.5)  ← PuzzleActor의 UI 위치
      ↓
[ProjectWorldToScreen]  ← 핵심 변환 함수!
      ↓
[Screen Coordinate]
      ↓
(425.28, 271.10)  ← 화면 픽셀 좌표
      ↓
[UIOffset 적용]
      ↓
(425.28, 251.10)  ← 최종 표시 위치
      ↓
[DrawTexture]
      ↓
화면에 "E" 렌더링! 

코드

1️⃣ Trace 실행 (InteractionComponent)

bool UInteractionComponent::TraceForInteractable(FHitResult& HitResult)
{
    // 카메라 위치와 방향 가져오기
    APlayerController* PlayerController = 
        OwnerCharacter->GetController<APlayerController>();
    FVector TraceStartLocation;
    FRotator TraceDirection;
    PlayerController->GetPlayerViewPoint(
        TraceStartLocation,  // 카메라 위치
        TraceDirection       // 카메라 방향
    );

    // Trace 끝점 계산 (앞으로 1000 단위)
    FVector TraceEndLocation = 
        TraceStartLocation + (TraceDirection.Vector() * InteractionDistance);

    // LineTrace 실행
    UKismetSystemLibrary::LineTraceSingle(
        this,
        TraceStartLocation,   // 시작점
        TraceEndLocation,     // 끝점
        ECC_Visibility,       // 추적 채널
        false,
        ActorsToIgnore,       // 플레이어는 무시
        EDrawDebugTrace::None,
        HitResult,            // 결과 저장
        true
    );

    // 결과 분석
    if (HitResult.bBlockingHit)
    {
        AActor* HitActor = HitResult.GetActor();

        // IInteractable 인터페이스 구현 여부 확인
        if (HitActor->Implements<UInteractable>())
        {
            return true;  // 상호작용 가능한 객체 발견!
        }
    }

    return false;  // 상호작용 가능한 객체 없음
}

2️⃣ 위치 계산 (MyProjectHUD)

// 3D 월드 좌표 → 2D 화면 좌표 변환
void AMyProjectHUD::UpdateInteractionUI()
{
    // ...

    // PuzzleActor의 UI 위치 가져오기 (월드 좌표)
    FVector WorldLocation = Interactable->GetUIWorldLocation();
    // 예: (750, -30, 124.5) ← 액터 위에서 약간 위쪽

    // 중요: ProjectWorldToScreen 사용!
    // 이 함수가 3D → 2D 변환을 담당합니다
    FVector2D ScreenPosition;
    PC->ProjectWorldLocationToScreen(WorldLocation, ScreenPosition);
    // 결과: (425.28, 271.10) ← 화면 좌표로 변환됨!

    // 오프셋 적용 (UI를 조금 위쪽에 표시)
    CachedScreenPosition = ScreenPosition + UIOffset;
    // UIOffset은 보통 (0, -50) 정도로 설정
}

3️⃣ UI 렌더링 (MyProjectHUD::DrawHUD)

void AMyProjectHUD::DrawHUD()
{
    // DrawHUD는 매 프레임 호출됨
    // 하지만 bShouldShowUI로 조건 제어

    if (!bShouldShowUI) return;

    // 텍스처 크기 조정 (중심을 기준으로)
    FVector2D DrawPosition = CachedScreenPosition - (UISize * 0.5f);
    // 예: CachedScreenPosition이 (425, 250)일 때
    //     UISize가 (50, 50)이면
    //     DrawPosition = (400, 225) ← 좌상단 코너

    // Canvas에 렌더링
    DrawTexture(
        InteractionTexture,
        DrawPosition.X,       // 400
        DrawPosition.Y,       // 225
        UISize.X,             // 50
        UISize.Y,             // 50
        0.0f,                 // UV X 시작
        0.0f,                 // UV Y 시작
        1.0f,                 // UV X 끝
        1.0f,                 // UV Y 끝
        FLinearColor::White   // 색상 (흰색)
    );
}

추가 문제점

  • 카메라 기준으로 라인 트레이스를 설정하니까 작은 액터를 정면에 맞춰 HUD 띄우는 게 힘들다
  • 캐릭터 기준으로 설정하기

배운점

인터페이스의 중요성

  • 다양한 상호작용 객체를 통일된 방식으로 처리
  • 새로운 상호작용 객체 추가 시 기존 코드 수정 불필요

2. Timer vs Tick

  • Tick: 매 프레임 실행 (성능 낭비)
  • Timer: 필요한 주기마다만 실행 (성능 최적화)

3. 좌표 변환의 중요성

  • 3D 월드 좌표 ≠ 2D 화면 좌표
  • ProjectWorldToScreen은 카메라 뷰를 고려한 변환

4. HUD 렌더링

  • DrawHUD: 매 프레임 호출 (UI 업데이트)
  • UpdateInteractionUI: 필요할 때만 호출 (데이터 업데이트)
  • 두 함수의 역할 분리 → 효율적인 구조

앞으로 추가 가능한 기능들

1. 여러 UI 요소

  • 상호작용 키 + 설명 텍스트
  • 거리 표시
  • 상태 아이콘

2. 상호작용 결과 UI

  • "잠금 해제됨" 메시지
  • 획득 아이템 표시
  • 상호작용 애니메이션

3. 성능 개선

  • LOD (Level of Detail) 적용
  • 화면 범위 밖 UI 생략
  • Culling 적용

0개의 댓글