TIL(56)

이채원·2026년 9월 9일

TIL

목록 보기
57/79

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


코드카타 정리

문자열 s가 주어졌을 때, s의 각 위치마다 자신보다 앞에 나왔으면서, 자신과 가장 가까운 곳에 있는 같은 글자가 어디 있는지 알고 싶습니다.
예를 들어, s="banana"라고 할 때, 각 글자들을 왼쪽부터 오른쪽으로 읽어 나가면서 다음과 같이 진행할 수 있습니다.

b는 처음 나왔기 때문에 자신의 앞에 같은 글자가 없습니다. 이는 -1로 표현합니다.
a는 처음 나왔기 때문에 자신의 앞에 같은 글자가 없습니다. 이는 -1로 표현합니다.
n은 처음 나왔기 때문에 자신의 앞에 같은 글자가 없습니다. 이는 -1로 표현합니다.
a는 자신보다 두 칸 앞에 a가 있습니다. 이는 2로 표현합니다.
n도 자신보다 두 칸 앞에 n이 있습니다. 이는 2로 표현합니다.
a는 자신보다 두 칸, 네 칸 앞에 a가 있습니다. 이 중 가까운 것은 두 칸 앞이고, 이는 2로 표현합니다.
따라서 최종 결과물은 [-1, -1, -1, 2, 2, 2]가 됩니다.

문자열 s이 주어질 때, 위와 같이 정의된 연산을 수행하는 함수 solution을 완성해주세요.

#include <string>
#include <vector>

using namespace std;

vector<int> solution(string s) {
    vector<int> answer;
    
    // a부터 z까지 알파벳이 가장 마지막으로 발견된 인덱스를 적어둘 배열
    // 아직 한 번도 보지 못했으니까 처음에는 다 -1로 해두기
    vector<int> last_position(26, -1); 

    for (int i = 0; i < s.length(); i++) {
        
        // 현재 글자가 26칸 중 몇 번째 칸인지 계산 (예: a는 0, b는 1)
        int alphabet_index = s[i] - 'a'; 

        // last_position 확인했는데 -1이면 (처음 나온 글자)
        if (last_position[alphabet_index] == -1) {
            answer.push_back(-1);
        } 
        // last_position에 숫자가 적혀 있다면 (예전에 나온 적이 있는 글자)
        else {
            // 현재 i에서 예전에 발견됐던 위치를 빼면 몇 칸 떨어져 있는지 거리계산
            int distance = i - last_position[alphabet_index];
            answer.push_back(distance);
        }

        // 똑같은 글자가 또 나올 경우
        // last_position에 적힌 글자의 마지막 발견 위치를 'i'로 업데이트
        last_position[alphabet_index] = i; 
    }

    return answer;
}

AI가 작성한 코드

#include <string>
#include <vector>

using namespace std;

vector<int> solution(string s) {
    vector<int> answer;
    
    // 1. 메모리 재할당(이사) 방지: 들어올 데이터의 크기를 아니까 미리 자리를 깔아둡니다.
    answer.reserve(s.length());

    // 알파벳 26개의 위치를 기억할 배열
    vector<int> last_pos(26, -1);

    for (int i = 0; i < s.length(); i++) {
        int char_idx = s[i] - 'a';

        // 2. if ~ else 문을 없애고 삼항 연산자( ? : )로 한 줄로 압축합니다.
        // 해석: 처음 나온 글자(-1)면? -1을 넣고 : 아니면? (현재 위치 - 예전 위치)를 넣어라.
        answer.push_back(last_pos[char_idx] == -1 ? -1 : i - last_pos[char_idx]);

        // 현재 위치로 수첩 갱신
        last_pos[char_idx] = i;
    }

    return answer;
}
  1. answer.reserve(s.length());
    문자열 길이가 10,000자라면, 이 코드가 없을 때 answer 배열은 자리가 꽉 찰 때마다 더 큰 메모리를 발급받고 기존 데이터를 복사해 이사하는 과정을 14번이나 겪게 됨.
    하지만 시작하자마자 reserve로 "10,000개 들어갈 거니까 미리 비워둬!"라고 선언하면, 복사 연산이 '0'이 되어 속도가 비약적으로 상승

  2. if-else 대신 삼항 연산자(? :) 사용
    정답 바구니에 데이터를 넣는다(push_back)는 하나의 명확한 목적을 가진 로직이 위에서 아래로 끊김 없이 흐르게 만들어 가독성 극대화

  3. Hash Map(unordered_map)을 쓰지 않은 이유
    "알파벳(Key)마다 위치(Value)를 저장하니까 해시 맵을 써야 하는 거 아냐?"라고 생각하기 쉽습니다. 하지만 다뤄야 할 Key가 '소문자 알파벳(딱 26개)'으로 고정되어 있다면 무겁고 복잡한 해시 맵 대신 그냥 26칸짜리 단순 배열을 쓰는 것이 압도적으로 빠릅니다. 해시 함수를 계산할 필요 없이 메모리 주소로 직행하기 때문입니다(Cache Friendly).


팀프로젝트 정리

가장 핵심 기능부터 살을 붙여나가는 순서로 코딩

1단계: 데이터 뼈대 및 기본 컴포넌트 세팅

  • WeaponData 구조체 선언: 헤더 파일에 무기 스탯(공격력, 탄창, 사거리 등)을 담을 구조체를 만들기
  • CombatComponent 클래스 생성 및 부착: 언리얼 에디터에서 컴포넌트를 생성하고 플레이어 캐릭터에 붙이기
  • BeginPlay: 게임이 시작될 때 currentWeapon에 임시 데이터를 넣고, bCanPrimaryAttack 등을 true로 초기화해서 바로 총을 쏠 수 있는 상태로 만들기

2단계: 전투 루프

탄약 제한(30발)이나 쿨타임은 나중에 하고 일단 마우스를 클릭했을 때 데미지가 들어가는지부터 확인하는 것부터 할 수 있게 만들기

  • PrimaryAttack 구현 (조건문 없이): 마우스 좌클릭 시 카메라 전방으로 LineTraceSingleByChannel 쏘는 로직 작성. 처음에는 DrawDebugLine 함수를 써서 쏜 궤적이 레이저로 화면에 보이게 만들면 테스트하기 편할 거 같음.
  • ProcessHit 연결: 레이저가 적중한 대상(HitResult) 받아오기
  • 데미지 전달 (ApplyDamage): 타겟에게 데미지를 넘겨주고, 타겟 몬스터의 체력이 실제로 깎이는지 확인.

3단계: 자원 및 상태 관리

총알이 계에에에속 나가는 것 막고 게임 규칙 세우기

  • 쿨타임 적용 (ResetPrimaryAttack): 공격 직후 bCanPrimaryAttack = false로 바꾸고 언리얼 타이머(GetWorld()->GetTimerManager().SetTimer) 달아서 공격 속도 제어.
  • 탄약 차감 및 재장전 (ReloadWeapon & FinishReload): 공격하면 currentAmmo를 1씩 깎고, 0이 되면 공격 못하게 막기. R키를 눌러 예비 탄약(totalAmmo)에서 탄창으로 총알을 옮기는 로직.
  • UI 연동 준비: 이 단계에서 GetCurrentAmmo(), GetTotalAmmo() 함수 완성해서 UI 담당자가 화면에 총알 갯수를 띄울 수 있게 해주기

4단계: 특수 스킬, 외부 시스템 연동

  • 특수 공격 (SpecialAttack): 헤드샷 모드(버프 스위치 bIsHeadshotModeActive)를 켜는 로직 추가, PrimaryAttack의 데미지 판정 조건문을 작성해주면서 즉사(9999 데미지) 처리. 투사체 스킬 ApplyAoEDamage까지 구현.
  • 인벤토리 연동: 장전이 끝났을 때(FinishReload), 인벤토리 담당자가 만든 ConsumeItem 함수를 호출해서 가방 안의 실제 총알 아이템 데이터를 -처리 해주기
  • 애니메이션 및 사운드 연결: 로직이 잘 돌면, 코딩해둔 함수 사이사이에 애니메이션 재생 코드와 사운드 출력 코드도 작성

대용량 탄창, 개머리판, 견착 등 파츠 생각하기

일단 1단계 뼈대부터 작업 시작!!!

  • 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")
    int32 TotalAmmo;    // 보유 중인 전체 예비 총알

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

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

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    float AttackRange;  // 사거리

    // --- 스킬 스탯 ---
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    float AoERadius;    // 범위 공격 반경

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
    float SpecialCooldownTime;  // 특수 스킬 쿨타임 

    FWeaponData()
    {
        CurrentAmmo = 30;
        MagazineCapacity = 30;
        TotalAmmo = 90;
        BaseDamage = 25.0f;
        AttackSpeed = 0.2f; // 0.2초마다 1발 발사
        AttackRange = 5000.0f;  
        AoERadius = 300.0f;
        SpecialCooldownTime = 10.0f;
    }
};

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ASSETMIGRATE_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 bCanSpecialAttack; // 스킬 사용 가능 여부 - 스킬 쿨타임 체크

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

    UPROPERTY(BlueprintReadOnly, Category = "Combat|State")
    bool bIsHeadshotModeActive; // 헤드샷 스킬 모드

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

    UFUNCTION(BlueprintPure, Category = "Combat|UI")
    int32 GetTotalAmmo() const;     // 보유 중인 전체 예비 총알 갯수

    UFUNCTION(BlueprintPure, Category = "Combat|UI")
    float GetWeaponDamage() const;  // 총 데미지
};
  • CombatComponent.cpp
#include "CombatComponent.h"

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

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

    bCanPrimaryAttack = true;
    bCanSpecialAttack = true;
    bIsReloading = false;
    bIsHeadshotModeActive = false;
}

// Getter 함수 구현

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

int32 UCombatComponent::GetTotalAmmo() const
{
    return CurrentWeapon.TotalAmmo;
}

float UCombatComponent::GetWeaponDamage() const
{
    // 헤드샷 모드일 경우 즉사 데미지 수치 리턴, 아닐 경우 기본 데미지 리턴
    if (bIsHeadshotModeActive)
    {
        return 9999.0f;
    }
    return CurrentWeapon.BaseDamage; 
}

플레이어 캐릭터 블루프린트에 방금 만든 Combat Component를 Attach
디테일 패널에 Weapon Stats 카테고리 - 장탄수나 데미지 조절 가능

마우스를 클릭하면 레이저가 나가고 맞은 적에게 데미지가 들어간다

  • CombatComponent.h
class ASSETMIGRATE_API UCombatComponent : public UActorComponent
{
public:
    // 기본 공격
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void PrimaryAttack();

    // 타격 처리 및 데미지 전달
    UFUNCTION(BlueprintCallable, Category = "Combat|Action")
    void ProcessHit(AActor* HitTarget, float AppliedDamage);
  • CombatComponent.cpp
#include "CombatComponent.h"
#include "Kismet/GameplayStatics.h" // ApplyDamage 쓰기 위해 추가
#include "DrawDebugHelpers.h"       // 레이저 선 그리기 위해 추가

void UCombatComponent::PrimaryAttack()
{
    AActor* Owner = GetOwner();
    if (Owner == nullptr) return;

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

    // 총알이 날아갈 도착 지점 계산 (현재 위치 + 시선 방향 * 사거리)
    FVector ShotDirection = EyeRotation.Vector();
    FVector TraceEnd = EyeLocation + (ShotDirection * CurrentWeapon.AttackRange);

    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(); // 1단계에서 만든 함수 재사용 (헤드샷 모드면 9999 반환)

        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);
    }
}

다음 학습 목표

플레이어 블루프린트의 마우스 좌클릭(Left Mouse Button) 이벤트에 컴포넌트를 드래그해 놓은 뒤 PrimaryAttack 함수를 연결

사거리 없게 수정

코드 작성한 거 에디터에서 확인

대용량 탄창, 개머리판, 견착 등 파츠 생각하기

profile
초보 개발자

0개의 댓글