TIL(58)

이채원·2026년 9월 11일

TIL

목록 보기
59/77

오늘의 할일
1. 코드카타 정리
2. 팀프로젝트 정리
3. 다음 학습 목표


코드카타 정리

콜라 빈 병 2개를 가져다주면 콜라 1병을 주는 마트가 있다. 빈 병 20개를 가져다주면 몇 병을 받을 수 있는가?

단, 보유 중인 빈 병이 2개 미만이면, 콜라를 받을 수 없다.

문제를 풀던 상빈이는 콜라 문제의 완벽한 해답을 찾았습니다. 상빈이가 푼 방법은 아래 그림과 같습니다. 우선 콜라 빈 병 20병을 가져가서 10병을 받습니다. 받은 10병을 모두 마신 뒤, 가져가서 5병을 받습니다. 5병 중 4병을 모두 마신 뒤 가져가서 2병을 받고, 또 2병을 모두 마신 뒤 가져가서 1병을 받습니다. 받은 1병과 5병을 받았을 때 남은 1병을 모두 마신 뒤 가져가면 1병을 또 받을 수 있습니다. 이 경우 상빈이는 총 10 + 5 + 2 + 1 + 1 = 19병의 콜라를 받을 수 있습니다.

문제를 열심히 풀던 상빈이는 일반화된 콜라 문제를 생각했습니다. 이 문제는 빈 병 a개를 가져다주면 콜라 b병을 주는 마트가 있을 때, 빈 병 n개를 가져다주면 몇 병을 받을 수 있는지 계산하는 문제입니다. 기존 콜라 문제와 마찬가지로, 보유 중인 빈 병이 a개 미만이면, 추가적으로 빈 병을 받을 순 없습니다. 상빈이는 열심히 고심했지만, 일반화된 콜라 문제의 답을 찾을 수 없었습니다. 상빈이를 도와, 일반화된 콜라 문제를 해결하는 프로그램을 만들어 주세요.

콜라를 받기 위해 마트에 주어야 하는 병 수 a, 빈 병 a개를 가져다 주면 마트가 주는 콜라 병 수 b, 상빈이가 가지고 있는 빈 병의 개수 n이 매개변수로 주어집니다. 상빈이가 받을 수 있는 콜라의 병 수를 return 하도록 solution 함수를 작성해주세요.

#include <string>
#include <vector>

using namespace std;

int solution(int a, int b, int n) {
    int answer = 0; // 최종적으로 받은 콜라의 총합

    // 빈 병(n)이 마트에서 요구하는 최소 개수(a)보다 크거나 같을 때
    while (n >= a) {
        
        // 현재 빈 병으로 마트에서 몇 번 교환할 수 있는지 (몫)
        int exchange_count = n / a; 
        
        // 이번 교환으로 새롭게 받게 되는 콜라의 수
        int new_cola = exchange_count * b; 
        
        // 남은 빈 병의 수 (나머지)
        int leftover_empty = n % a; 
        
        // 새로 받은 콜라 더해주기
        answer += new_cola; 
        
        // 새 콜라를 다 마셨으니, 빈 병 업데이트
        // (방금 받은 새 콜라 빈 병 + 아까 교환 못하고 남겨둔 빈 병)
        n = new_cola + leftover_empty; 
    }

    return answer;
}

AI가 작성한 코드

using namespace std;

int solution(int a, int b, int n) {
    // 가진 빈 병이 교환 조건(a)보다 적으면 애초에 불가능하므로 0 반환
    if (n < a) return 0;
    
    // while문(반복) 없이 단 한 줄의 수학 공식으로 정답을 바로 계산합니다.
    return ((n - b) / (a - b)) * b;
}
  1. 순지출(Net Cost)의 개념:
    빈 병 a개를 주고 b개를 돌려받는다는 것은 결과적으로 한 번 교환할 때마다 내 손에서 a - b개의 빈 병이 영구적으로 사라진다는 뜻. 예를 들어 3개를 주고 1개를 받는다면(a=3, b=1), 실제로는 한 번에 2개씩 줄어듦

  2. 왜 분자가 n - b 인가?:
    단순히 n / (a - b)로 나누면 안 됩니다. 마트에서 교환을 성사시키려면 내 손에 최소한 a개의 병이 쥐어져 있어야 하기 때문
    마지막 교환을 하고 나서 돌려받는 b개는 더 이상 "순지출"의 재료로 쓸 수 없는 일종의 보증금 같은 존재. 따라서 전체 병 n에서 마지막에 돌려받을 b개를 미리 빼놓은 상태(n - b)에서, 순지출(a - b)로 나누어주면 정확히 몇 번 교환할 수 있는지 횟수가 나옴

  3. 마지막으로 b 곱하기:
    구해진 교환 횟수에 1회당 받는 콜라 수(b)를 곱해주면, 총 콜라의 개수 도출


팀프로젝트 정리

일단 탄창 스탯을 아이템 쪽으로 넘기는 걸로 바꿔서 탄창 스탯이 필요한 부분은 인벤토리 파일에서 끌어오는 방향으로 하기로 했음

4단계: 권총 재장전 및 인벤토리 연동

  • CombatComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CombatComponent.generated.h"

// 무기 기본 데이터 구조체
USTRUCT(BlueprintType)
struct FWeaponData
{
    GENERATED_BODY()

public:
    // --- 탄약 스탯 ---
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    int32 CurrentAmmo;  // 현재 탄창의 총알 수

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    int32 MagazineCapacity; // 탄창 최대 용량

    // --- 공격 스탯 ---
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    float BaseDamage;   // 기본 공격력

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    float AttackSpeed;  // 공격 속도

    FWeaponData()
    {
        CurrentAmmo = 30;
        MagazineCapacity = 30;
        BaseDamage = 25.0f;
        AttackSpeed = 0.2f; // 0.2초마다 1발 발사
    }
};

// 전투 총괄 관리 컴포넌트
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DEAD_HOSPITAL_API UCombatComponent : public UActorComponent
{
	GENERATED_BODY()

public:
    UCombatComponent();

protected:
    virtual void BeginPlay() override;

public:
    // 무기 데이터 및 상태 변수
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    FWeaponData CurrentWeapon;  // 현재 장착 중인 무기 데이터 참조

    UPROPERTY(BlueprintReadOnly, Category = "Combat|State")
    bool bCanPrimaryAttack; // 기본 공격 가능 여부 - 연사 쿨타임 체크

    UPROPERTY(BlueprintReadOnly, Category = "Combat|State")
    bool bIsReloading;  // 재장전 중인지 여부 - 장전 중 공격 불가능

    // 외부 시스템 연동용 Getter 함수 (UI, 인벤토리 등)
    UFUNCTION(BlueprintPure, Category = "Combat|UI")
    int32 GetCurrentAmmo() const;   // 총알 갯수

    UFUNCTION(BlueprintPure, Category = "Combat|UI")
    float GetWeaponDamage() const;  // 총 데미지

    // 기본 공격
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void PrimaryAttack();

    // 타격 처리 및 데미지 전달
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void ProcessHit(AActor* HitTarget, float AppliedDamage);

    // 쿨타임 복구 함수
    UFUNCTION()
    void ResetPrimaryAttack();
    
    // 장전 시작
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void ReloadWeapon();

private:
    // 장전 완료 콜백
    UFUNCTION()
    void FinishReload();

    // 타이머 제어용 핸들 (재장전 소요 시간(딜레이))
    FTimerHandle TimerHandle_Reload;
    
    // 타이머 제어용 핸들 (총의 연사 속도(발사 딜레이))
    FTimerHandle TimerHandle_PrimaryCooldown;
};
  • CombatComponent.cpp
#include "CombatComponent.h"
#include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h"
#include "TimerManager.h"
#include "InventoryComponent.h"
#include "Engine/World.h"

UCombatComponent::UCombatComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
}

void UCombatComponent::BeginPlay()
{
    Super::BeginPlay();

    bCanPrimaryAttack = true;
    bIsReloading = false;
}

// Getter 함수 구현

int32 UCombatComponent::GetCurrentAmmo() const
{
    return CurrentWeapon.CurrentAmmo;
}

float UCombatComponent::GetWeaponDamage() const
{
    return CurrentWeapon.BaseDamage;
}

void UCombatComponent::PrimaryAttack()
{
    // 발사 조건 검사 (쿨타임 대기 중이거나, 장전 중이거나, 총알이 0개면 발사 불가)
    if (bCanPrimaryAttack == false || bIsReloading == true)
    {
        return;
    }

    if (CurrentWeapon.CurrentAmmo <= 0)
    {
        // 총알이 없을 때의 처리 (빈 총 소리 등)
        UE_LOG(LogTemp, Warning, TEXT("(탄약이 없습니다)"));
        // 4단계에서 자동 재장전 함수(ReloadWeapon)를 여기에 연결

        return;
    }


    // 조건 충족 시: 상태 변경 및 탄약 차감
    bCanPrimaryAttack = false;
    CurrentWeapon.CurrentAmmo -= 1;

    // 쿨타임 타이머 시작 (공격 속도 시간만큼 대기 후 ResetPrimaryAttack 실행)
    GetWorld()->GetTimerManager().SetTimer(
        TimerHandle_PrimaryCooldown,
        this,
        &UCombatComponent::ResetPrimaryAttack,
        CurrentWeapon.AttackSpeed,
        false
    );

    AActor* Owner = GetOwner();
    if (Owner == nullptr) return;

    // 플레이어의 카메라 위치와 회전값 가져오기
    FVector EyeLocation;
    FRotator EyeRotation;
    Owner->GetActorEyesViewPoint(EyeLocation, EyeRotation);
    
    // 무한한 사거리 (시선 방향 벡터 * 999999.0f)
	FVector ShotDirection = EyeRotation.Vector();
	FVector TraceEnd = EyeLocation + (ShotDirection * 999999.0f);

	FHitResult HitResult;
	FCollisionQueryParams QueryParams;
	QueryParams.AddIgnoredActor(Owner); // 내가 쏜 총에 내가 맞지 않도록 예외 처리

	// 레이캐스트(보이지 않는 선) 발사
	bool bSuccess = GetWorld()->LineTraceSingleByChannel(
    	HitResult,
    	EyeLocation,
    	TraceEnd,
    	ECC_Visibility, // 눈에 보이는 물체들과 충돌
    	QueryParams
	);

	// 테스트용 빨간색 궤적 그리기 (에디터에서 눈으로 확인하기 위함, 2초간 유지)
	DrawDebugLine(GetWorld(), EyeLocation, TraceEnd, FColor::Red, false, 2.0f, 0, 2.0f);

	if (bSuccess)
	{
    	// 맞은 지점에 초록색 점 표시
    	DrawDebugPoint(GetWorld(), HitResult.ImpactPoint, 20.0f, FColor::Green, false, 2.0f);

    	// 어떤 액터를 맞췄는지, 데미지는 얼마를 줘야 하는지 계산해서 ProcessHit로 넘김
    	AActor* HitActor = HitResult.GetActor();
    	float DamageToApply = GetWeaponDamage(); 
    	ProcessHit(HitActor, DamageToApply);
    }
}

void UCombatComponent::ProcessHit(AActor* HitTarget, float AppliedDamage)
{
    if (HitTarget)
    {
        // 데미지 전달 함수 호출
        UGameplayStatics::ApplyDamage(
            HitTarget,               // 맞는 대상
            AppliedDamage,           // 데미지 수치
            GetOwner()->GetInstigatorController(), // 공격한 사람의 컨트롤러
            GetOwner(),              // 공격한 사람(무기 또는 플레이어)
            UDamageType::StaticClass()
        );

        // 출력 로그 창에 맞은 대상의 이름과 들어간 데미지를 글자로 띄움 (확인용)
        UE_LOG(LogTemp, Warning, TEXT("타격 성공! 맞은 대상: %s, 데미지: %f"), *HitTarget->GetName(), AppliedDamage);
    }
}

void UCombatComponent::ResetPrimaryAttack()
{
    // 타이머 시간이 다 되면 다시 사격할 수 있도록 상태 복구
    bCanPrimaryAttack = true;
}

void UCombatComponent::ReloadWeapon()
{
    // 이미 탄창이 꽉 찼거나 장전 중이면 무시
    if (CurrentWeapon.CurrentAmmo >= CurrentWeapon.MagazineCapacity || bIsReloading == true)
    {
        return;
    }

    // 플레이어 액터에서 인벤토리 컴포넌트 찾아오기
    UInventoryComponent* InventoryComponent = GetOwner()->FindComponentByClass<UInventoryComponent>();
    if (InventoryComponent == nullptr) return;
    
    // 가방에 예비 총알이 있는지 검사
    int32 AmmoCount = InventoryComponent->GetItemQuantity("Ammo");
    if (AmmoCount <= 0)
    {
        UE_LOG(LogTemp, Warning, TEXT("장전 실패: 가방에 총알이 없습니다"));
        return;
    }

    // 장전 상태 진입 (사격 불가)
    bIsReloading = true;
    UE_LOG(LogTemp, Warning, TEXT("재장전 중..."));
    
    // 장전 대기 시간(2초) 타이머 시작
    GetWorld()->GetTimerManager().SetTimer(
        TimerHandle_Reload,
        this,
        &UCombatComponent::FinishReload,
        2.0f,
        false
    );
}

void UCombatComponent::FinishReload()
{
    UInventoryComponent* InventoryComponent = GetOwner()->FindComponentByClass<UInventoryComponent>();
    if (InventoryComponent == nullptr)
    {
        bIsReloading = false;
        return;
    }

    // 탄창에 채워야 할 빈 공간 계산
    int32 NeededAmmo = CurrentWeapon.MagazineCapacity - CurrentWeapon.CurrentAmmo;

    // 가방에 남아있는 총알 갯수 확인
    int32 AmmoCount = InventoryComponent->GetItemQuantity("Ammo");

    // 실제 장전할 수량(ReloadAmount) 결정
    // 빈 공간(NeededAmmo)과 가방 속 총알(AmmoCount) 중 더 작은 값을 선택
    int32 ReloadAmount = FMath::Min(NeededAmmo, AmmoCount);

    // 탄창에 총알 추가
    CurrentWeapon.CurrentAmmo += ReloadAmount;

    // 가방에서 실제로 소비한 총알 차감
    InventoryComponent->RemoveItem("Ammo", ReloadAmount);

    // 장전 상태 해제
    bIsReloading = false;
    UE_LOG(LogTemp, Warning, TEXT("장전 완료. 현재 탄창: %d발"), CurrentWeapon.CurrentAmmo);
}

5단계: 자동 재장전 함수 & 시각/청각 이펙트 추가

  • CombatComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CombatComponent.generated.h"

// 무기 기본 데이터 구조체
USTRUCT(BlueprintType)
struct FWeaponData
{
    GENERATED_BODY()

public:
    // --- 탄약 스탯 ---
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    int32 CurrentAmmo;  // 현재 탄창의 총알 수

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    int32 MagazineCapacity; // 탄창 최대 용량

    // --- 공격 스탯 ---
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    float BaseDamage;   // 기본 공격력

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    float AttackSpeed;  // 공격 속도

    FWeaponData()
    {
        CurrentAmmo = 30;
        MagazineCapacity = 30;
        BaseDamage = 25.0f;
        AttackSpeed = 0.2f; // 0.2초마다 1발 발사
    }
};

// 전투 총괄 관리 컴포넌트
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DEAD_HOSPITAL_API UCombatComponent : public UActorComponent
{
	GENERATED_BODY()

public:
    UCombatComponent();

protected:
    virtual void BeginPlay() override;

public:
    // 무기 데이터 및 상태 변수
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    FWeaponData CurrentWeapon;  // 현재 장착 중인 무기 데이터 참조

    UPROPERTY(BlueprintReadOnly, Category = "Combat|State")
    bool bCanPrimaryAttack; // 기본 공격 가능 여부 - 연사 쿨타임 체크

    UPROPERTY(BlueprintReadOnly, Category = "Combat|State")
    bool bIsReloading;  // 재장전 중인지 여부 - 장전 중 공격 불가능

    // 외부 시스템 연동용 Getter 함수 (UI, 인벤토리 등)
    UFUNCTION(BlueprintPure, Category = "Combat|UI")
    int32 GetCurrentAmmo() const;   // 총알 갯수

    UFUNCTION(BlueprintPure, Category = "Combat|UI")
    float GetWeaponDamage() const;  // 총 데미지

    // UI 경고 메시지 띄우기 (탄약이 없습니다)
    UFUNCTION(BlueprintImplementableEvent, Category = "Combat|UI")
    void OnAmmoEmptyWarning();

    // 기본 공격
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void PrimaryAttack();

    // 타격 처리 및 데미지 전달
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void ProcessHit(AActor* HitTarget, float AppliedDamage);

    // 쿨타임 복구 함수
    UFUNCTION()
    void ResetPrimaryAttack();

    // 장전 시작
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void ReloadWeapon();

    // 시각/청각 에셋
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Effects")
    class USoundBase* FireSound; // 사격 소리

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Effects")
    class USoundBase* EmptySound; // 빈 총 찰칵 소리

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Effects")
    class USoundBase* ReloadSound; // 재장전 소리

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Effects")
    class UParticleSystem* HitEffect; // 피격 이펙트 (피 튀김, 스파크)

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Animation")
    class UAnimMontage* FireAnimation; // 사격 애니메이션

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Animation")
    class UAnimMontage* ReloadAnimation; // 재장전 애니메이션

private:
    // 장전 완료 콜백
    UFUNCTION()
    void FinishReload();

    // 타이머 제어용 핸들 (재장전 소요 시간(딜레이))
    FTimerHandle TimerHandle_Reload;

    // 타이머 제어용 핸들 (총의 연사 속도(발사 딜레이))
    FTimerHandle TimerHandle_PrimaryCooldown;
};
  • CombatComponent.cpp
#include "CombatComponent.h"
#include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h"
#include "TimerManager.h"
#include "InventoryComponent.h"
#include "Engine/World.h"
#include "Sound/SoundBase.h"
#include "Particles/ParticleSystem.h"
#include "Animation/AnimMontage.h"
#include "GameFramework/Character.h"

UCombatComponent::UCombatComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
}

void UCombatComponent::BeginPlay()
{
    Super::BeginPlay();

    bCanPrimaryAttack = true;
    bIsReloading = false;
}

// Getter 함수 구현

int32 UCombatComponent::GetCurrentAmmo() const
{
    return CurrentWeapon.CurrentAmmo;
}

float UCombatComponent::GetWeaponDamage() const
{
    return CurrentWeapon.BaseDamage;
}

void UCombatComponent::PrimaryAttack()
{
    // 발사 조건 검사 (쿨타임 대기 중이거나, 장전 중이거나, 총알이 0개면 발사 불가)
    if (bCanPrimaryAttack == false || bIsReloading == true)
    {
        return;
    }

    if (CurrentWeapon.CurrentAmmo <= 0)
    {
        // 총알이 없을 때의 처리 (빈 총 소리 등)
        UE_LOG(LogTemp, Warning, TEXT("(탄약이 없습니다)"));
        
        // 빈 총 사운드 재생
        if (EmptySound)
        {
            UGameplayStatics::PlaySoundAtLocation(this, EmptySound, GetOwner()->GetActorLocation());
        }

        // 탄약이 0일 때 자동 재장전 실행
        ReloadWeapon();
        return;
    }

    // 조건 충족 시: 상태 변경 및 탄약 차감
    bCanPrimaryAttack = false;
    CurrentWeapon.CurrentAmmo -= 1;

    // 쿨타임 타이머 시작 (공격 속도 시간만큼 대기 후 ResetPrimaryAttack 실행)
    GetWorld()->GetTimerManager().SetTimer(
        TimerHandle_PrimaryCooldown,
        this,
        &UCombatComponent::ResetPrimaryAttack,
        CurrentWeapon.AttackSpeed,
        false
    );

    AActor* Owner = GetOwner();
    if (Owner == nullptr) return;

    // 플레이어의 카메라 위치와 회전값 가져오기
    FVector EyeLocation;
    FRotator EyeRotation;
    Owner->GetActorEyesViewPoint(EyeLocation, EyeRotation);

    // 총기 사운드 및 애니메이션 재생
    if (FireSound)
    {
        UGameplayStatics::PlaySoundAtLocation(this, FireSound, EyeLocation);
    }

    ACharacter* OwnerCharacter = Cast<ACharacter>(Owner);
    if (OwnerCharacter && FireAnimation)
    {
        OwnerCharacter->PlayAnimMontage(FireAnimation);
    }

    // 무한한 사거리 (시선 방향 벡터 * 999999.0f)
    FVector ShotDirection = EyeRotation.Vector();
    FVector TraceEnd = EyeLocation + (ShotDirection * 999999.0f);

    FHitResult HitResult;
    FCollisionQueryParams QueryParams;
    QueryParams.AddIgnoredActor(Owner); // 내가 쏜 총에 내가 맞지 않도록 예외 처리

    // 레이캐스트(보이지 않는 선) 발사
    bool bSuccess = GetWorld()->LineTraceSingleByChannel(
        HitResult,
        EyeLocation,
        TraceEnd,
        ECC_Visibility, // 눈에 보이는 물체들과 충돌
        QueryParams
    );

    // 테스트용 빨간색 궤적 그리기 (에디터에서 눈으로 확인하기 위함, 2초간 유지)
    DrawDebugLine(GetWorld(), EyeLocation, TraceEnd, FColor::Red, false, 2.0f, 0, 2.0f);

    if (bSuccess)
    {
        // 맞은 지점에 초록색 점 표시
        DrawDebugPoint(GetWorld(), HitResult.ImpactPoint, 20.0f, FColor::Green, false, 2.0f);

        // 어떤 액터를 맞췄는지, 데미지는 얼마를 줘야 하는지 계산해서 ProcessHit로 넘김
        AActor* HitActor = HitResult.GetActor();
        float DamageToApply = GetWeaponDamage(); 
        ProcessHit(HitActor, DamageToApply);

        // 타격 위치에 피 튀김/스파크 이펙트 생성
        if (HitEffect)
        {
            UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), HitEffect, HitResult.ImpactPoint);
        }
    }
}

void UCombatComponent::ProcessHit(AActor* HitTarget, float AppliedDamage)
{
    if (HitTarget)
    {
        // 데미지 전달 함수 호출
        UGameplayStatics::ApplyDamage(
            HitTarget,               // 맞는 대상
            AppliedDamage,           // 데미지 수치
            GetOwner()->GetInstigatorController(), // 공격한 사람의 컨트롤러
            GetOwner(),              // 공격한 사람(무기 또는 플레이어)
            UDamageType::StaticClass()
        );

        // 출력 로그 창에 맞은 대상의 이름과 들어간 데미지를 글자로 띄움 (확인용)
        UE_LOG(LogTemp, Warning, TEXT("타격 성공! 맞은 대상: %s, 데미지: %f"), *HitTarget->GetName(), AppliedDamage);
    }
}

void UCombatComponent::ResetPrimaryAttack()
{
    // 타이머 시간이 다 되면 다시 사격할 수 있도록 상태 복구
    bCanPrimaryAttack = true;
}

void UCombatComponent::ReloadWeapon()
{
    // 이미 탄창이 꽉 찼거나 장전 중이면 무시
    if (CurrentWeapon.CurrentAmmo >= CurrentWeapon.MagazineCapacity || bIsReloading == true)
    {
        return;
    }

    // 플레이어 액터에서 인벤토리 컴포넌트 찾아오기
    UInventoryComponent* InventoryComponent = GetOwner()->FindComponentByClass<UInventoryComponent>();
    if (InventoryComponent == nullptr) return;

    // 가방에 예비 총알이 있는지 검사
    int32 AmmoCount = InventoryComponent->GetItemQuantity("Ammo");
    if (AmmoCount <= 0)
    {
        UE_LOG(LogTemp, Warning, TEXT("장전 실패: 가방에 총알이 없습니다"));

        // 장전 실패 시에도 찰칵 소리 재생
        if (EmptySound)
        {
            UGameplayStatics::PlaySoundAtLocation(this, EmptySound, GetOwner()->GetActorLocation());
        }
        return;
    }

    // 장전 상태 진입 (사격 불가)
    bIsReloading = true;
    UE_LOG(LogTemp, Warning, TEXT("재장전 중..."));

    // 재장전 사운드 및 애니메이션 재생
    if (ReloadSound)
    {
        UGameplayStatics::PlaySoundAtLocation(this, ReloadSound, GetOwner()->GetActorLocation());
    }

    ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
    if (OwnerCharacter && ReloadAnimation)
    {
        OwnerCharacter->PlayAnimMontage(ReloadAnimation);
    }

    // 장전 대기 시간(2초) 타이머 시작
    GetWorld()->GetTimerManager().SetTimer(
        TimerHandle_Reload,
        this,
        &UCombatComponent::FinishReload,
        2.0f,
        false
    );
}

void UCombatComponent::FinishReload()
{
    UInventoryComponent* InventoryComponent = GetOwner()->FindComponentByClass<UInventoryComponent>();
    if (InventoryComponent == nullptr)
    {
        bIsReloading = false;
        return;
    }

    // 탄창에 채워야 할 빈 공간 계산
    int32 NeededAmmo = CurrentWeapon.MagazineCapacity - CurrentWeapon.CurrentAmmo;

    // 가방에 남아있는 총알 갯수 확인
    int32 AmmoCount = InventoryComponent->GetItemQuantity("Ammo");

    // 실제 장전할 수량(ReloadAmount) 결정
    // 빈 공간(NeededAmmo)과 가방 속 총알(AmmoCount) 중 더 작은 값을 선택
    int32 ReloadAmount = FMath::Min(NeededAmmo, AmmoCount);

    // 탄창에 총알 추가
    CurrentWeapon.CurrentAmmo += ReloadAmount;

    // 가방에서 실제로 소비한 총알 차감
    InventoryComponent->RemoveItem("Ammo", ReloadAmount);

    // 장전 상태 해제
    bIsReloading = false;
    UE_LOG(LogTemp, Warning, TEXT("장전 완료. 현재 탄창: %d발"), CurrentWeapon.CurrentAmmo);
}

여기까지 1차 구현 목표 달성 완료..

그리고 새롭게 정리된 기획서에 따르면 내가 구현해야하는 기능을 이렇게 정리할 수 있다

내일 구현해야할 것들

1. 보스전 및 특수 기믹

  • 보스 몬스터 데미지 및 사망 델리게이트:
    보스 몬스터의 체력이 0이 되었을 때, 단순 사망 애니메이션 재생으로 끝나는 게 아니라 보스가 죽는 순간에 탈출 문이 생성되고 타이머가 시작되어야 하기 때문에 보스 사망 시 게임 모드나 레벨 블루프린트가 탈출 이벤트를 발동시킬 수 있도록 연결할 수 있게 코드를 작성
  • 보스의 심장 파괴 로직:
    스토리 기획에 심장을 파괴해 보스의 재생을 못하게 하는 게 있어서 심장이 맵에 배치되는 파괴 가능한 Actor라면 총기 사격(PrimaryAttack의 LineTrace)에 맞았을 때 데미지를 입고 파괴되도록 심장 전용 TakeDamage 함수와 체력 변수를 구현

2. 은신 시스템과의 상태 연동 (Stealth State Control)

  • 은신 중 무기 사용 제한:
    플레이어가 캐비닛이나 책상 밑에 숨어있을 때는 총을 쏘거나 장전할 수 없어야 합니다. CombatComponent에 bool bIsHidden 같은 상태 변수를 추가하고, 타 팀원(상호작용 담당자)이 숨기 버튼을 누를 때 이 값을 true로 바꿔주도록 해야 합니다. 발사 조건문(PrimaryAttack)에 && [bIsHidden == false]를 추가해야 합니다.

3. 사망 판정 및 전용 게임오버 연동 (Death Handling)

  • 사망 원인(Cause of Death) 구분:
    기획서에 따르면 타임오버 사망과 적에게 맞아 죽는 사망의 게임오버 텍스트(Tip)가 다릅니다. 플레이어 캐릭터의 TakeDamage 함수 내부에서 체력이 0이 될 때, "적의 공격으로 죽었다"는 명확한 신호(Enum 또는 bool)를 게임오버 UI 쪽에 전달해 주어야 합니다.
  • 적 처치(Kill) 카운트 전달:
    이전 대화에서 논의한 대로, 일반 좀비나 몬스터의 TakeDamage 함수에서 체력이 0이 되면 GameMode의 TotalKills 변수를 1 증가시키는 코드를 몬스터 클래스에 작성해야 합니다.

4. 타 파트와의 세부 조율이 필요한 사항 (협의 필요)

  • 조준/사격/장전 시 이동속도 페널티:
    이동 속도(걷기/조깅)가 기획되어 있는데, 총을 쏘거나 장전하는 동안 뛸 수 있게 놔둘 것인지, 아니면 강제로 걷기 속도로 늦출 것인지 캐릭터 이동 담당자와 협의가 필요합니다. 만약 속도를 줄여야 한다면 PrimaryAttack과 ReloadWeapon 실행 시 이동 컴포넌트의 최대 속도(MaxWalkSpeed)를 제어하는 코드가 추가되어야 합니다.

다음 학습 목표

profile
초보 개발자

0개의 댓글