StrikForge 프로젝트/작업일지

UE5 RTS 유닛 자동 전투 개선 및 주변 아군 타겟 공유 설계

editor66484 2026. 9. 2. 23:01

오늘은 RTS 유닛 전투 시스템을 전체적으로 점검하고, 플레이어 유닛의 자동 공격이 자연스럽게 이어지도록 개선 방향을 정리했다.

주요 목표는 다음과 같다.

  • 플레이어 및 적 AI의 자동 전투 구조 확인
  • 뒤쪽 유닛이 전투에 참여하지 않는 원인 분석
  • 일반 이동 중 적 발견 시 자동 교전하도록 개선
  • 한 유닛이 발견한 적을 주변 아군에게 공유하는 기능 설계
  • 유닛이 한 대상에게 몰릴 때 발생할 수 있는 문제 검토

1. 현재 유닛 전투 구조 확인

프로젝트의 팀 구분은 TeamIndex를 사용한다.

진영TeamIndex
플레이어 0
적 AI 1

양쪽 유닛 모두 공격 자체는 자동으로 수행한다.

플레이어 유닛

  • 플레이어가 목적지를 지정할 수 있다.
  • 주변에서 적 유닛이나 적 건물을 발견하면 자동으로 공격한다.
  • 일반 이동과 공격 이동을 구분할 수 있다.

적 AI 유닛

  • 스폰 시스템을 통해 생성된다.
  • 플레이어 유닛이나 건물을 탐색한다.
  • 적을 발견하면 자동으로 이동하고 공격한다.

2. 뒤쪽 유닛이 공격하지 않는 문제

여러 플레이어 유닛이 뭉쳐 있을 때 앞쪽 유닛은 적을 발견하고 공격하지만, 뒤쪽 유닛은 가만히 서 있는 문제가 있었다.

뒤쪽 유닛을 직접 선택하여 앞으로 이동시키면 정상적으로 적을 발견하고 공격하는 것은 확인했다.

이를 통해 공격 기능 자체보다는 다음 문제가 원인일 가능성이 높다고 판단했다.

  • 뒤쪽 유닛이 AggroRadius 밖에 있음
  • 각 유닛이 서로 독립적으로 적을 탐색함
  • 앞쪽 유닛의 적 발견 정보가 뒤쪽 유닛에게 전달되지 않음

즉, 앞 유닛이 교전을 시작하더라도 뒤 유닛은 자신의 탐색 범위 안에 적이 들어오기 전까지 전투에 참여하지 않는 구조였다.


3. 일반 이동을 공격 이동으로 처리

플레이어가 유닛을 이동시켰을 때 이동 도중 적을 발견하면 전투에 참여할 수 있도록, 일반 이동 명령을 공격 이동으로 전달하는 방향으로 수정했다.

수정 위치:

SFInGamePlayerController.cpp

기존 코드:

case ESFUnitCommandMode::Move:
    UnitCommand = ESFUnitCommand::Move;
    break;

변경 코드:

case ESFUnitCommandMode::Move:
    UnitCommand = ESFUnitCommand::AttackMove;
    break;

이 변경을 적용하면 플레이어 유닛은 목적지로 이동하면서도 주변 적을 탐색할 수 있다.

다만 일반 이동과 공격 이동이 완전히 같은 동작이 되므로, 추후에는 다음과 같이 명령을 다시 구분할 필요가 있다.

  • 일반 이동: 적을 무시하고 목적지까지 이동
  • 공격 이동: 이동 중 적을 발견하면 교전
  • 강제 공격: 플레이어가 지정한 대상만 추적

4. 주변 아군에게 적 공유 기능 설계

뒤쪽 유닛까지 자연스럽게 전투에 참여시키기 위해, 한 유닛이 발견한 적을 주변 아군에게 전달하는 기능을 설계했다.

전체 처리 과정은 다음과 같다.

```mermaid
flowchart LR
    A["유닛이 적 발견"] --> B["주변 아군 검색"]
    B --> C["TeamIndex 비교"]
    C --> D["같은 팀의 유닛만 선택"]
    D --> E["발견한 적 전달"]
    E --> F["아군의 CurrentTarget 설정"]
    F --> G["경로 재탐색"]
    G --> H["전투 합류"]
```

이번 기능은 전 맵의 모든 아군에게 적을 전달하는 방식이 아니다.

AllyAssistRadius 안에 있는 같은 팀의 유닛만 지원 요청을 받도록 설계했다.


5. UnitBase에 추가한 선언

UnitBase.h에 적 공유를 담당할 함수 두 개를 선언했다.

/** 발견한 적을 일정 거리 안의 아군에게grade 전달한다 */
void AlertNearbyAllies(AActor* InTarget);

/** 주변 아군의 지원 요청으로 전달된 적을 받아들인다 */
void ReceiveAssistTarget(AActor* InTarget);

각 함수의 역할은 다음과 같다.

AlertNearbyAllies

자신이 발견한 적을 주변 아군에게 전달한다.

ReceiveAssistTarget

다른 아군에게 전달받은 적이 유효한지 확인하고 자신의 공격 대상으로 받아들인다.


6. 아군 지원 범위 추가

UnitBase.h의 전투 설정 영역에 아군 지원 범위를 추가했다.

/** 주변 아군이 발견한 적에게 합류하는 거리 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat",
    meta = (AllowPrivateAccess = true, ClampMin = "0.0"))
float AllyAssistRadius = 1500.f;

기존 적 탐색 범위는 다음과 같다.

float AggroRadius = 1050.f;

두 범위의 역할은 서로 다르다.

설정역할
AggroRadius 유닛이 직접 적을 발견하는 범위
AllyAssistRadius 아군이 발견한 적 정보를 전달받는 범위

초기 테스트에서는 전투 참여 범위가 너무 넓어지지 않도록 AllyAssistRadius를 약 1200.f부터 확인하는 것이 좋다.

float AllyAssistRadius = 1200.f;

뒤쪽 유닛의 반응이 부족하면 1300 → 1500 순서로 증가시킬 예정이다.


7. SFCombatSubsystem 활용

프로젝트에는 이미 공간 그리드를 이용하여 주변 아군을 찾는 함수가 구현되어 있었다.

void GetNearbyAlliesForSeparation(
    const AActor* InSelf,
    int32 InTeamIndex,
    float InRadius,
    TArray<AActor*>& OutActors
) const;

이 함수는 다음 조건을 검사한다.

  • 전달받은 TeamIndex와 같은 유닛
  • CombatUnit 유형
  • 지정한 반경 안에 있는 유닛
  • 자기 자신이 아닌 유닛
  • 현재 유효한 액터

따라서 월드에 존재하는 모든 유닛을 직접 반복하지 않고, 기존 전투 서브시스템의 공간 검색 기능을 활용할 수 있다.

다만 함수 이름에 ForSeparation이 포함되어 있기 때문에 추후 구조를 정리할 때는 범용 주변 아군 조회 함수를 따로 만드는 것도 고려할 수 있다.

void GetNearbyAlliedUnits(
    const AActor* InSelf,
    int32 InTeamIndex,
    float InRadius,
    TArray<AActor*>& OutActors
) const;

8. 주변 아군에게 적 전달

AlertNearbyAllies()에서는 전투 서브시스템을 통해 주변 아군을 찾고, 각 아군에게 적을 전달하도록 설계했다.

void AUnitBase::AlertNearbyAllies(AActor* InTarget)
{
    if (!IsTargetUsable(InTarget))
    {
        return;
    }

    USFCombatSubsystem* Combat = USFCombatSubsystem::Get(this);
    if (!IsValid(Combat))
    {
        return;
    }

    TArray<AActor*> NearbyAllies;

    Combat->GetNearbyAlliesForSeparation(
        this,
        GetTeamIndex(),
        AllyAssistRadius,
        NearbyAllies
    );

    for (AActor* AllyActor : NearbyAllies)
    {
        AUnitBase* AllyUnit = Cast<AUnitBase>(AllyActor);
        if (!IsValid(AllyUnit))
        {
            continue;
        }

        AllyUnit->ReceiveAssistTarget(InTarget);
    }
}

공유받은 유닛이 다시 주변에 전달하지 않도록 구성하여, 전 맵으로 지원 요청이 연쇄적으로 퍼지는 현상을 방지했다.


9. 공유된 적을 공격 대상으로 설정

ReceiveAssistTarget()에서는 지원 요청을 무조건 받아들이지 않고 현재 유닛 상태를 검사해야 한다.

void AUnitBase::ReceiveAssistTarget(AActor* InTarget)
{
    if (CurrentCombatState == EUnitCombatState::Dead)
    {
        return;
    }

    if (!IsTargetUsable(InTarget))
    {
        return;
    }

    if (IsValid(ForcedTarget))
    {
        return;
    }

    if (bHoldGround)
    {
        return;
    }

    if (IsTargetUsable(CurrentTarget))
    {
        return;
    }

    CurrentTarget = InTarget;
    RetargetTimer = RetargetInterval;

    bSightBlockedReported = false;
    bRangeUnreachableReported = false;
    bNoTargetReported = false;
    bAdvanceTargetReported = false;

    if (AUnitAIController* AI = GetUnitAI())
    {
        AI->ForceRepath();
    }
}

다음 우선순위를 유지하도록 했다.

  1. 플레이어가 직접 지정한 ForcedTarget
  2. 현재 교전 중인 CurrentTarget
  3. 주변 아군에게 전달받은 지원 대상
  4. 일반 자동 탐색 대상

10. 공격 시 끊김 가능성 검토

주변 아군에게 같은 적을 전달하면 여러 유닛이 동시에 하나의 적에게 접근할 수 있다.

이 과정에서 발생할 수 있는 현상은 다음과 같다.

  • 여러 유닛이 하나의 적 주변으로 몰림
  • 공격 위치를 확보하지 못한 유닛이 계속 이동함
  • 좁은 공간에서 분리 조향과 공격 이동이 충돌함
  • 공유 범위가 너무 넓으면 먼 유닛까지 전투에 끌려감
  • 경로 재탐색이 반복되면 순간적으로 이동이 끊김

이를 방지하기 위해 다음 조건을 적용했다.

if (IsTargetUsable(CurrentTarget))
{
    return;
}

이미 정상적으로 싸우고 있는 유닛은 전달받은 대상으로 타겟을 변경하지 않는다.

또한 ForceRepath()는 공유된 대상을 처음 받아들였을 때 한 번만 실행하고, StopMovement()는 호출하지 않는다. 이로 인해 공격 도중 이동이 반복적으로 끊기는 현상을 줄일 수 있다.


오늘의 작업 결과

오늘은 실제 전투 문제를 다음 두 단계로 나누어 개선 방향을 잡았다.

1차 수정

플레이어 유닛이 이동 중 적을 발견하면 자동으로 교전할 수 있도록 일반 이동 명령을 공격 이동으로 연결했다.

2차 수정

앞쪽 유닛이 발견한 적을 일정 범위 안의 아군에게 공유하여, 탐색 범위 밖에 있던 뒤쪽 유닛도 전투에 합류할 수 있도록 구조를 설계했다.

현재 UnitBase.h에는 관련 함수 선언과 AllyAssistRadius 설정이 추가된 상태다. 다음 작업에서는 UnitBase.cpp의 함수 구현과 실제 호출 부분을 완성한 뒤 에디터에서 전투 테스트를 진행할 예정이다.


다음 테스트 항목

  • 아군 네 명을 앞뒤로 배치
  • 앞쪽 유닛만 적을 직접 발견하도록 설정
  • 뒤쪽 유닛이 공유받은 적에게 이동하는지 확인
  • TeamIndex가 다른 유닛에게 정보가 전달되지 않는지 확인
  • 공격 중인 유닛의 타겟이 불필요하게 변경되지 않는지 확인
  • 유닛이 제자리에서 떨거나 반복적으로 멈추는지 확인
  • 적 제거 후 다음 타겟을 정상적으로 탐색하는지 확인
  • AllyAssistRadius를 1200, 1300, 1500 순서로 비교

느낀 점

유닛이 자동 공격 기능을 가지고 있더라도 각 유닛이 독립적으로만 적을 탐색하면 부대 단위의 전투처럼 보이지 않을 수 있다.

이번 작업을 통해 개별 유닛의 탐색 범위를 무작정 넓히는 것보다, 적을 먼저 발견한 유닛이 제한된 범위 안의 아군에게 정보를 전달하도록 만드는 것이 자연스러운 집단 전투 구현에 더 적합하다는 점을 확인했다.