티스토리 뷰

🟩 구현 목표

  • FGrenadeStat 구조체와 데이터 테이블을 활용하여 코드 수정 없는 발사체 확장 시스템을 구축한다.
  • 물리 엔진의 IgnoreActorWhenMoving을 사용하여 발사체의 자가 충돌 및 폭발 문제를 해결한다.
  • FTimerHandle을 이용한 스택형 자동 충전 시스템을 구현하여 게임플레이의 전략적 요소를 더한다.

🟧 1. 데이터 기반 설계: 구조체와 데이터 테이블 활용

다양한 발사체(수류탄, 유도탄 등)를 유연하게 추가하기 위해 모든 성능 지표와 에셋 정보를 FTableRowBase를 상속받은 구조체로 데이터화했다.

// GrenadeStat.h 핵심 구조체 정의
USTRUCT(BlueprintType)
struct FGrenadeStat : public FTableRowBase
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TSubclassOf<class AGrenadeProjectile> GrenadeClass; // 발사체 클래스

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Damage; // 폭발 데미지

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float CooldownTime; // 발사 간격

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float RegenTime; // 1발 충전 시간

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 MaxCharges; // 최대 보유량

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr<class UParticleSystem> ExplosionEffect; // 폭발 효과
};

🟧 2. 발사체 물리 충돌 예외 처리

발사체가 생성되는 즉시 캐릭터 본인의 콜리전과 충돌하여 터지는 문제를 방지하기 위해 물리 엔진 수준에서 액터 무시 설정을 적용했다.

// GrenadeProjectile.cpp: 물리 엔진 예외 처리
void AGrenadeProjectile::BeginPlay()
{
    Super::BeginPlay();

    AActor* MyOwner = GetOwner();
    if (MyOwner)
    {
        // 물리 엔진에게 주인(플레이어)은 이동 시 무시하도록 명령
        if (SphereComp)
        {
            SphereComp->IgnoreActorWhenMoving(MyOwner, true);
        }
        
        // 주인 캐릭터의 루트 컴포넌트에서도 이 발사체를 무시하게 설정
        if (MyOwner->GetRootComponent())
        {
            UPrimitiveComponent* OwnerRoot = Cast<UPrimitiveComponent>(MyOwner->GetRootComponent());
            if (OwnerRoot)
            {
                OwnerRoot->IgnoreActorWhenMoving(this, true);
            }
        }
    }
}

🟧 3. 소켓 기반 발사 및 오프셋 보정

어깨 런처 소켓(grenade_front)에서 발사될 때, 시각적 자연스러움과 물리적 안전을 위해 전방 오프셋을 적용하고 발사체에 데이터를 전달한다.

// WeaponComponent.cpp: 발사 위치 계산 및 스폰
void UWeaponComponent::LaunchGrenade()
{
    FVector SpawnLocation = OwnerChar->GetMesh()->GetSocketLocation(TEXT("grenade_front"));
    // 캐릭터 정면으로 20만큼 밀어내어 자가 충돌 방지 및 자연스러운 연출
    SpawnLocation += OwnerChar->GetActorForwardVector() * 20.f; 
    FRotator SpawnRotation = OwnerChar->GetActorRotation();

    FActorSpawnParameters Params;
    Params.Owner = OwnerChar;
    Params.Instigator = OwnerChar;

    AGrenadeProjectile* Projectile = GetWorld()->SpawnActor<AGrenadeProjectile>(
        Stat->GrenadeClass, SpawnLocation, SpawnRotation, Params);

    if (Projectile)
    {
        // 데이터 테이블에서 로드한 스탯을 생성된 발사체에 직접 주입
        Projectile->Damage = Stat->Damage;
        Projectile->ExplosionEffect = Stat->ExplosionEffect;
    }
}

🟧 4. 스택형 자동 충전(Regeneration) 시스템

사용 후 일정 시간이 지나면 수류탄이 자동으로 하나씩 충전되는 로직을 FTimerHandle로 구현하여 전투의 지속성을 확보했다.

// WeaponComponent.cpp: 자동 충전 시스템
void UWeaponComponent::RegenerateGrenade()
{
    if (CurrentGrenadeCount < MaxCharges)
    {
        CurrentGrenadeCount++;
        UE_LOG(LogTemp, Log, TEXT("수류탄 1개 충전됨 현재 개수 : %d / %d"), CurrentGrenadeCount, Stat->MaxCharges);

        // 최대 보유량에 도달하면 타이머를 중지하여 자원 낭비 최적화
        if (CurrentGrenadeCount >= MaxCharges)
        {
            GetWorld()->GetTimerManager().ClearTimer(GrenadeRegenTimerHandle);
            UE_LOG(LogTemp, Log, TEXT("수류탄 충전 완료"));
        }
    }
}

🟧핵심 요약

  • 데이터 기반 설계: 모든 무기와 기술의 수치를 데이터 테이블화하여 코드 수정 없이 밸런스 튜닝이 가능하다.
  • 물리 안정성: 단순한 충돌 체크 로직을 넘어 물리 엔진의 예외 처리 API를 활용해 자가 충돌 버그를 근본적으로 차단했다.
  • 전투 흐름 최적화: 자동 충전 시스템을 도입함으로써 플레이어가 자원을 아끼면서도 적절한 타이밍에 스킬을 쏟아붓는 전투 환경을 조성했다.