StrikForge 프로젝트/작업일지

[Unreal Engine 5] TeamIndex 기반 자원 관리와 3D 유닛 프리뷰 구현

editor66484 2026. 8. 24. 00:11

오늘은 RTS 프로젝트의 팀별 자원 관리 구조를 정리하고, Render Target과 Scene Capture Component를 활용해 UI에 3D 유닛 모델을 표시하는 작업을 진행했다.

1. TeamIndex 기반 상태 관리

기존 자원 데이터가 하나의 공용 값으로 관리되면 플레이어와 AI의 자원이 섞일 수 있다.

예를 들어 AI가 광물을 획득했는데 플레이어의 광물까지 증가하는 문제가 발생할 수 있다. 이를 방지하기 위해 GameState에서 TeamIndex를 기준으로 각 팀의 데이터를 분리하도록 구조를 설계했다.

  • TeamIndex 0: 플레이어
  • TeamIndex 1: 적 AI
  • 필요하면 이후 다른 팀도 추가 가능

전체 구조는 다음과 같다.

flowchart LR A["자원 획득"] --> B["TeamIndex 확인"] B --> C["GameState"] C --> D{"TeamIndex"} D -->|"0"| E["플레이어 자원"] D -->|"1"| F["적 AI 자원"]

중요한 점은 TeamIndex가 종족을 나타내는 값이 아니라 해당 액터가 어느 팀에 소속되어 있는지 구분하는 값이라는 것이다.

종족까지 구분해야 한다면 추후 RaceType 같은 별도의 열거형을 사용하는 것이 적절하다.


2. 팀별 자원 구조체 추가

각 팀의 자원을 하나로 묶어 관리할 수 있도록 FSFTeamResourceState 구조체를 구성했다.

USTRUCT(BlueprintType)
struct FSFTeamResourceState
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "SF|Resource")
    int32 Mineral = 0;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "SF|Resource")
    int32 Ether = 0;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "SF|Population")
    int32 CurrentPopulation = 0;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "SF|Population")
    int32 MaximumPopulation = 10;
};

구조체에 포함한 데이터는 다음과 같다.

변수역할
Mineral 일반적인 건설 및 유닛 생산에 사용하는 광물
Ether 특정 건물이나 고급 기능에 사용하는 희귀 자원
CurrentPopulation 현재 생산된 유닛이 사용하는 인구수
MaximumPopulation 현재 생산할 수 있는 최대 인구수

SF 접두어와 Pascal Case 규칙을 적용해 프로젝트의 명명 규칙도 유지했다.


3. GameState에서 팀별 자원 저장

싱글플레이 RTS에서도 플레이어와 AI의 자원 상태는 인게임 도중 계속 변경된다. 따라서 게임 진행 상태를 관리하는 GameState에 팀별 자원 구조체를 저장하도록 구성했다.

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "SF|Resource")
TMap<int32, FSFTeamResourceState> TeamResources;

TMap의 구성은 다음과 같다.

TeamIndex → 해당 팀의 자원 정보

예를 들면 다음과 같이 저장된다.

0 → 플레이어의 광물, 에테르, 인구수
1 → 적 AI의 광물, 에테르, 인구수

팀별 자원 조회는 다음과 같은 흐름으로 처리한다.

const FSFTeamResourceState* TeamResource =
    TeamResources.Find(TeamIndex);

if (TeamResource == nullptr)
{
    return 0;
}

return TeamResource->Mineral;

이 구조를 사용하면 자원을 획득하거나 소비할 때 반드시 TeamIndex를 함께 전달할 수 있다.

AddMineral(TeamIndex, Amount);
TrySpendResource(TeamIndex, MineralCost, EtherCost);
AddPopulation(TeamIndex, PopulationCost);

UI는 플레이어 팀인 TeamIndex 0의 값만 가져와 출력하면 된다.


4. Render Target을 활용한 3D 유닛 프리뷰

다음으로 미니맵 하단 UI에 선택된 유닛의 3D 모델을 보여주는 기능을 구성했다.

일반적인 이미지 파일을 사용하는 방식이 아니라 별도의 카메라로 실제 유닛 모델을 촬영하고, 그 화면을 UI에 출력하는 방식이다.

전체 흐름은 다음과 같다.

```mermaid
flowchart LR
    A["3D 유닛 모델"] --> B["Scene Capture Component 2D"]
    B --> C["Texture Render Target 2D"]
    C --> D["UI Material"]
    D --> E["UMG Image"]
    E --> F["미니맵 하단 유닛 프리뷰"]
```

이 방식은 정적인 초상화와 다르게 실제 3D 모델과 애니메이션을 UI 안에서 보여줄 수 있다는 장점이 있다.


5. Scene Capture Component 사용

3D 모델을 촬영하기 위해 프리뷰 전용 액터에 다음 컴포넌트를 구성했다.

SFUnitPreviewActor
├─ Scene Component
├─ Skeletal Mesh Component
└─ Scene Capture Component 2D

Scene Capture Component 2D는 게임 월드를 별도의 카메라 시점으로 촬영하고, 결과를 Texture Render Target 2D에 전달한다.

주요 설정은 다음과 같다.

  • Texture Target: 생성한 Render Target 지정
  • Capture Source: Final Color 사용
  • Projection Type: Perspective
  • Primitive Render Mode: 필요한 액터만 렌더링
  • Capture Every Frame: 애니메이션을 표시할 경우 활성화
  • Capture On Movement: 움직일 때만 갱신할 경우 사용

프리뷰에 필요한 유닛만 출력하려면 Show Only Actors를 활용할 수 있다. 이렇게 하면 실제 게임 맵의 배경이나 불필요한 오브젝트가 Render Target에 함께 나타나는 것을 방지할 수 있다.


6. Render Target을 UI에 연결

Scene Capture Component 2D에서 촬영한 화면을 UMG에 표시하기 위해 다음 순서로 연결했다.

  1. Texture Render Target 2D 생성
  2. Render Target을 사용하는 UI Material 생성
  3. Material Domain을 User Interface로 설정
  4. Render Target을 Texture Sample에 연결
  5. UI 위젯의 Image Brush에 해당 Material 적용
SceneCapture2D
    ↓
Texture Render Target 2D
    ↓
UI Material
    ↓
UMG Image

최종적으로 미니맵 하단의 Image 위젯을 통해 선택된 유닛의 3D 모델을 표시할 수 있다.


7. 3D 애니메이션 적용 방법 조사

3D 유닛 프리뷰가 정지된 모델로만 보이지 않도록 Skeletal Mesh Component에 애니메이션을 적용하는 방법도 확인했다.

적용 방식은 크게 두 가지가 있다.

Animation Blueprint 사용

실제 인게임 유닛과 동일한 애니메이션 블루프린트를 적용하는 방식이다.

PreviewMesh->SetAnimInstanceClass(AnimationBlueprintClass);

다만 인게임 애니메이션 블루프린트가 이동 속도나 AI Controller 상태를 요구한다면 프리뷰 액터에서는 정상적으로 동작하지 않을 수 있다.

단일 Animation Sequence 재생

프리뷰용 Idle 애니메이션 하나만 반복 재생하는 방식이다.

PreviewMesh->SetAnimationMode(
    EAnimationMode::AnimationSingleNode
);

PreviewMesh->PlayAnimation(
    PreviewIdleAnimation,
    true
);

유닛 프리뷰에서는 복잡한 인게임 상태가 필요하지 않기 때문에, 우선은 Idle Animation Sequence를 반복 재생하는 방식이 더 단순하고 안정적이다.


작업하면서 확인한 점

팀 데이터와 종족 데이터는 분리해야 한다

TeamIndex는 플레이어와 적 AI를 구분하기 위한 값이다. 비스트, 인간 등의 종족 정보를 함께 처리하려면 별도의 RaceType이 필요하다.

UI가 자원 값을 직접 변경하면 안 된다

UI는 GameState에서 자원 값을 조회하고 화면에 표시하는 역할만 담당해야 한다. 실제 자원 증감은 채집 건물, 생산 시스템 또는 게임 로직을 통해 처리해야 한다.

Scene Capture는 성능을 고려해야 한다

Capture Every Frame을 사용하면 매 프레임 Render Target을 갱신한다. 유닛 프리뷰가 항상 표시될 필요가 없다면 다음과 같이 최적화할 수 있다.

  • 선택된 유닛이 바뀔 때만 캡처
  • 결과창이나 HUD가 열려 있을 때만 활성화
  • Render Target 해상도를 필요 이상으로 크게 설정하지 않기
  • 프리뷰용 조명과 메시만 렌더링하기

RTS 게임에서는 화면에 많은 유닛이 존재하므로, 프리뷰 UI까지 불필요하게 매 프레임 갱신하지 않도록 주의해야 한다.


작업 결과

이번 작업을 통해 다음 구조를 정리했다.

  • GameState에서 TeamIndex별 데이터 분리
  • 광물, 에테르, 현재 인구수, 최대 인구수를 하나의 구조체로 관리
  • 플레이어와 AI의 자원이 서로 섞이지 않는 구조 마련
  • Render Target을 이용한 3D 유닛 프리뷰 흐름 구성
  • Scene Capture Component 2D와 UMG Image 연결 방식 확인
  • 프리뷰 유닛에 Idle 애니메이션을 적용하는 방법 조사

다음 작업에서는 실제 자원 채집 시스템이 TeamIndex를 전달하도록 연결하고, 선택된 유닛이 변경될 때 프리뷰 메시와 애니메이션도 함께 변경되도록 확장할 예정이다.