오늘의 할일
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;
}
순지출(Net Cost)의 개념:
빈 병 a개를 주고 b개를 돌려받는다는 것은 결과적으로 한 번 교환할 때마다 내 손에서 a - b개의 빈 병이 영구적으로 사라진다는 뜻. 예를 들어 3개를 주고 1개를 받는다면(a=3, b=1), 실제로는 한 번에 2개씩 줄어듦
왜 분자가 n - b 인가?:
단순히 n / (a - b)로 나누면 안 됩니다. 마트에서 교환을 성사시키려면 내 손에 최소한 a개의 병이 쥐어져 있어야 하기 때문
마지막 교환을 하고 나서 돌려받는 b개는 더 이상 "순지출"의 재료로 쓸 수 없는 일종의 보증금 같은 존재. 따라서 전체 병 n에서 마지막에 돌려받을 b개를 미리 빼놓은 상태(n - b)에서, 순지출(a - b)로 나누어주면 정확히 몇 번 교환할 수 있는지 횟수가 나옴
마지막으로 b 곱하기:
구해진 교환 횟수에 1회당 받는 콜라 수(b)를 곱해주면, 총 콜라의 개수 도출
일단 탄창 스탯을 아이템 쪽으로 넘기는 걸로 바꿔서 탄창 스탯이 필요한 부분은 인벤토리 파일에서 끌어오는 방향으로 하기로 했음
#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;
};
#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);
}
#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;
};
#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차 구현 목표 달성 완료..
그리고 새롭게 정리된 기획서에 따르면 내가 구현해야하는 기능을 이렇게 정리할 수 있다
PrimaryAttack의 LineTrace)에 맞았을 때 데미지를 입고 파괴되도록 심장 전용 TakeDamage 함수와 체력 변수를 구현CombatComponent에 bool bIsHidden 같은 상태 변수를 추가하고, 타 팀원(상호작용 담당자)이 숨기 버튼을 누를 때 이 값을 true로 바꿔주도록 해야 합니다. 발사 조건문(PrimaryAttack)에 && [bIsHidden == false]를 추가해야 합니다.TakeDamage 함수 내부에서 체력이 0이 될 때, "적의 공격으로 죽었다"는 명확한 신호(Enum 또는 bool)를 게임오버 UI 쪽에 전달해 주어야 합니다.TakeDamage 함수에서 체력이 0이 되면 GameMode의 TotalKills 변수를 1 증가시키는 코드를 몬스터 클래스에 작성해야 합니다.PrimaryAttack과 ReloadWeapon 실행 시 이동 컴포넌트의 최대 속도(MaxWalkSpeed)를 제어하는 코드가 추가되어야 합니다.