오늘의 할일
1. 코드카타 정리
2. 세션 정리
3. 궁금증 해결
<첫번째 코드카타>
문자열 s의 길이가 4 혹은 6이고, 숫자로만 구성돼있는지 확인해주는 함수
#include <string>
#include <vector>
using namespace std;
bool solution(string s) {
bool answer = true;
if (s.length() != 4 && s.length() != 6) {
answer = false;
}
for (char c : s) {
if (c < '0' || c > '9') {
answer = false;
}
}
return answer;
}
AI가 작성한 코드
#include <string>
using namespace std;
bool solution(string s) {
// 1. 길이가 4 또는 6이 아니면 즉시 실패 처리 (Early Return)
if (s.length() != 4 && s.length() != 6) return false;
// 2. "0123456789"에 포함되지 않는 문자가 '없음(npos)'인지 확인
return s.find_first_not_of("0123456789") == string::npos;
}
반복문 없는 극강의 가독성: for문을 해석할 필요 없이 "숫자가 아닌 문자를 처음으로 찾았는데, 그게 없어야(npos) 통과구나"라고 1초 만에 의도를 파악.
추가 헤더 불필요: <cctype>이나 <algorithm> 같은 다른 도구 상자를 가져올 필요 없이, 기본으로 주어지는 <string> 헤더 하나만으로 모든 작업이 끝납니다.
비즈니스 로직에 적합: find_first_not_of라는 내장 함수는 문자열 검증이라는 목적을 가장 직관적으로 보여주는 도구.
C++의 문자열에서 무언가를 찾는 함수들(예: find, find_first_not_of)은 목표물을 발견하면 그 글자가 있는 '위치 번호(인덱스)'를 반환
원하는 것을 못 찾았을 때
0을 돌려주자니, "0번째 위치에서 찾았다"는 뜻이 되고-1을 돌려주자니, size_t는 양수만 가질 수 있어서 음수를 담을 수가 없음그래서 size_t라는 그릇이 담을 수 있는 최댓값을 꽉 채워서 반환하면, 그걸 "못 찾았다!"라는 신호로 취급하기로 한 것.
이 최댓값을 매번 외우고 다닐 수 없으니, 알아보기 쉽게 string::npos라는 이름표를 붙여준 것.
// 1. "0123456789" (숫자)가 아닌 글자를 찾아봐!
// 2. 그 결과가 npos (못 찾음) 이랑 똑같아? -> 모두 숫자라는 뜻!
return s.find_first_not_of("0123456789") == string::npos;
"숫자가 아닌 불순물을 찾아라!"라고 명령했는데, 컴퓨터가 다 뒤져보고는 "npos입니다! (불순물이 없는데요!)"라고 보고하는 상황입니다. 불순물이 없으니 문자열 전체가 완벽한 숫자로만 이루어져 있다고 증명되는 것.
<두번째 코드카타>
행렬의 덧셈은 행과 열의 크기가 같은 두 행렬의 같은 행, 같은 열의 값을 서로 더한 결과가 됩니다. 2개의 행렬 arr1과 arr2를 입력받아, 행렬 덧셈의 결과를 반환하는 함수 완성해주세요.
이중반복문이라 많이 복잡하지만 다른 방법이 안 떠올랐고 일단 진행함
#include <string>
#include <vector>
using namespace std;
vector<vector<int>> solution(vector<vector<int>> arr1, vector<vector<int>> arr2) {
vector<vector<int>> answer = arr1;
for (size_t i = 0; i < arr1.size(); ++i) {
for (size_t j = 0; j < arr1[i].size(); ++j) {
answer[i][j] += arr2[i][j];
}
}
return answer;
}
AI가 작성한 코드
#include <vector>
#include <algorithm>
using namespace std;
vector<vector<int>> solution(vector<vector<int>> arr1, vector<vector<int>> arr2) {
for (size_t i = 0; i < arr1.size(); ++i) {
// arr1의 i번째 행과 arr2의 i번째 행을 더해서(plus), 다시 arr1에 덮어씌웁니다.
transform(arr1[i].begin(), arr1[i].end(), arr2[i].begin(), arr1[i].begin(), plus<int>());
}
return arr1;
}
의도의 명확성 (What, not How):
이중 for문과 [i][j]를 쓰면 코드를 읽는 사람이 '아, 인덱스를 돌면서 뭔가를 계산하고 있구나'라고 기계적인 흐름을 먼저 파악해야 합니다. 하지만 transform과 plus<int>()를 사용하면 "두 배열을 가져와서 더해라(plus)!"라고 단번에 알아볼 수 있음.
인덱스 실수 원천 차단:
가로세로 인덱스(i, j)를 헷갈려서 프로그램이 터지는 실수를 아예 할 수 없도록 구조적으로 막아버립니다. 반복자가 알아서 처음부터 끝까지 안전하게 탐색.
극강의 메모리 효율:
answer라는 새로운 2차원 배열을 만들지 않고, 매개변수로 주어진 arr1을 재활용하여 덮어씌운 뒤 그대로 반환하기 때문에 메모리 낭비가 없습니다.
transform(
arr1[i].begin(), // ① 1번 재료의 시작점: "arr1의 i번째 줄 처음부터..."
arr1[i].end(), // ② 1번 재료의 끝점: "...끝까지 가져와."
arr2[i].begin(), // ③ 2번 재료의 시작점: "arr2의 i번째 줄도 처음부터 짝을 맞춰서 가져와."
arr1[i].begin(), // ④ 결과를 담을 위치: "계산이 끝나면 다시 arr1의 처음 위치부터 차곡차곡 덮어씌워."
plus<int>() // ⑤ 할 일: "가져온 두 숫자를 서로 더해(+)."
);
요약
"arr1의 이번 줄(①~②)과 arr2의 이번 줄(③)을 순서대로 하나씩 꺼내서 더한 다음(⑤), 그 결과를 다시 arr1의 원래 자리(④)에 덮어씌워 줘(transform)"
모든 아이템 클래스의 부모인 베이스아이템 클래스에 컴포넌트 구현해주기
씬 컴포넌트(루트)와 스태틱 메쉬 컴포넌트 필요하므로 구현
콜리전 담당 컴포넌트도 추가
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"
#include "BaseItem.generated.h"
class USphereComponent;
UCLASS()
class TESTPROJECT2_API ABaseItem : public AActor, public IItemInterface
{
GENERATED_BODY()
public:
ABaseItem();
protected:
virtual void OnItemOverlap(AActor* OverlapActor) override;
virtual void OnItemEndOverlap(AActor* OverlapActor) override;
virtual void ActivateItem(AActor* Activator) override;
virtual FName GetItemType() const override;
virtual void DestroyItem();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FName ItemType;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
USceneComponent* Scene;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
USphereComponent* Collision;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
UStaticMeshComponent* StaticMesh;
};
#include "BaseItem.h"
#include "Components/SphereComponent.h"
ABaseItem::ABaseItem()
{
PrimaryActorTick.bCanEverTick = false;
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
SetRootComponent(Scene);
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
Collision->SetupAttachment(Scene);
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMesh->SetupAttachment(Collision);
}
ABaseItem::ABaseItem()
{
PrimaryActorTick.bCanEverTick = false;
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
SetRootComponent(Scene);
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
Collision->SetupAttachment(Scene);
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMesh->SetupAttachment(Collision);
//이벤트 바인딩
//이벤트 바인딩을 해주는 이유
//게임 실행 전에는 사실 어떤 객체가 어떤 상황에서 이벤트를 발생시킬지 전혀 알 수 없음
//플레이어가 움직이다가 특정 아이템과 충돌하거나 아니면 새로 생성된 적(예시)과 부딪히게 되는 상황들이 많이 발생
//근데 그 어떤 객체, 어떤 함수인지 알 수가 없음, 그래서 그때그때 객체는 생성됐다 사라졌다 그러는데 그런 예측 불가능한 상황에
//대비하려면 이 이벤트랑 함수를 게임 실행 중에 동적으로 연결을 해줘야함
//따라서 일종의 동적으로 게임 환경 상에서 실시간으로 이 함수가 이어지고 그리고 그 객체로부터 함수를 부르는 것 = 이벤트 바인딩
Collision->OnComponentBeginOverlap();
Collision->OnComponentEndOverlap();
}
class TESTPROJECT2_API IItemInterface
{
GENERATED_BODY()
public:
virtual void OnItemOverlap(
UPrimitiveComponent* OverlappedComp, //스피어 컴포넌트(오버랩이 발생한 자기 자신)
AActor* OtherActor, //콜리전 컴포넌트(스피어 컴포넌트에 부딪힌 상대방의 액터)
UPrimitiveComponent* OtherComp, //충돌을 일으킨 원인인 컴포넌트, 캡슐 컴포넌트
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) = 0;
virtual void OnItemEndOverlap(
UPrimitiveComponent* OverlappedComp, //스피어 컴포넌트
AActor* OtherActor, //콜리전 컴포넌트
UPrimitiveComponent* OtherComp, //충돌을 일으킨 원인인 컴포넌트, 캡슐 컴포넌트
int32 OtherBodyIndex) = 0;
virtual void ActivateItem(AActor* Activator) = 0;
virtual FName GetItemType() const = 0;
};
인터페이스 클래스가 바뀌었기 때문에 베이스아이템 헤더도 매개변수 추가해주기
class TESTPROJECT2_API ABaseItem : public AActor, public IItemInterface
{
GENERATED_BODY()
public:
ABaseItem();
protected:
virtual void OnItemOverlap(
UPrimitiveComponent* OverlappedComp, //스피어 컴포넌트
AActor* OtherActor, //콜리전 컴포넌트
UPrimitiveComponent* OtherComp, //충돌을 일으킨 원인인 컴포넌트, 캡슐 컴포넌트
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) override;
virtual void OnItemEndOverlap(
UPrimitiveComponent* OverlappedComp, //스피어 컴포넌트
AActor* OtherActor, //콜리전 컴포넌트
UPrimitiveComponent* OtherComp, //충돌을 일으킨 원인인 컴포넌트, 캡슐 컴포넌트
int32 OtherBodyIndex) override;
virtual void ActivateItem(AActor* Activator) override;
virtual FName GetItemType() const override;
ABaseItem::ABaseItem()
{
Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);
}
void ABaseItem::OnItemOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) {
}
void ABaseItem::OnItemEndOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex) {
}
void ABaseItem::OnItemOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) {
if (OtherActor && OtherActor->ActorHasTag("Player")) {
ActivateItem(OtherActor);
}
}
class TESTPROJECT2_API IItemInterface
{
GENERATED_BODY()
public:
UFUNCTION()
virtual void OnItemOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) = 0;
UFUNCTION()
virtual void OnItemEndOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex) = 0;
virtual void ActivateItem(AActor* Activator) = 0;
virtual FName GetItemType() const = 0;
};
여기까지 베이스 아이템의 기본적인 오버랩 충돌 처리 완료!!!
** 플레이어 태그 붙여주기(에디터에서)
다 적용이 잘 됐는지 로그 찍어보기!
void ABaseItem::OnItemOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) {
if (OtherActor && OtherActor->ActorHasTag("Player")) {
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Overlap!!!")));
ActivateItem(OtherActor);
}
}
아이템들에 약간의 처리 해주기
잘 되어있나 체크해주기
일단 CoinItem.h 파일에 ActivateItem을 오버라이딩 해줘야함
그래야만 내부에서 각자의 코인마다 동작을 설정해줄 수 있음
UCLASS()
class TESTPROJECT2_API ACoinItem : public ABaseItem
{
GENERATED_BODY()
public:
ACoinItem();
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 PointValue;
virtual void ActivateItem(AActor* Activator) override;
};
<임시로 값을 초기화해주기 (안전을 위해서)>
#include "CoinItem.h"
ACoinItem::ACoinItem() {
PointValue = 0;
ItemType = "DefaultCoin";
}
void ACoinItem::ActivateItem(AActor* Activator) {
if (Activator && Activator->ActorHasTag("Player")) {
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Player gained %d points!!"), PointValue));
DestroyItem();
}
}
코인 클래스 손봤으니까 밑에 자식 클래스는 크게 손 보지 않아도 됨
#include "BigCoinItem.h"
ABigCoinItem::ABigCoinItem() {
PointValue = 50;
ItemType = "BigCoin";
}
void ABigCoinItem::ActivateItem(AActor* Activator) {
Super::ActivateItem(Activator);
}
#include "SmallCoinItem.h"
ASmallCoinItem::ASmallCoinItem() {
PointValue = 50;
ItemType = "SmallCoin";
}
void ASmallCoinItem::ActivateItem(AActor* Activator) {
Super::ActivateItem(Activator);
}
#include "HealingItem.h"
AHealingItem::AHealingItem() {
HealAmount = 20.0f;
ItemType = "Healing";
}
void AHealingItem::ActivateItem(AActor* Activator) {
if (Activator && Activator->ActorHasTag("Player")) {
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Player gained %d HP"), HealAmount));
DestroyItem();
}
}
UCLASS()
class TESTPROJECT2_API AMineItem : public ABaseItem
{
GENERATED_BODY()
public:
AMineItem();
USphereComponent* ExplosionCollision;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 ExplosionDelay;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 ExplosionRadius;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 ExplosionDamage;
// 타이머 핸들러 선언
FTimerHandle ExplosionTimerHandle;
virtual void ActivateItem(AActor* Activator) override;
//폭발 함수 따로 지정해주기
void Explode();
};
#include "MineItem.h"
#include "Components/SphereComponent.h"
AMineItem::AMineItem() {
ExplosionDelay = 5.0f;
ExplosionRadius = 300.0f;
ExplosionDamage = 30.0f;
ItemType = "Mine";
ExplosionCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ExplosionCollision"));
ExplosionCollision->InitSphereRadius(ExplosionRadius);
ExplosionCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
ExplosionCollision->SetupAttachment(Scene);
}
void AMineItem::ActivateItem(AActor* Activator) {
// 타이머 핸들러 = 각자의 하나의 시계
// 게임 월드 -> 타이머 매니저 -> 타이머 핸들러 관리
// Activate가 되고 5초 뒤에 실행
// 이 타이머를 세팅해주기
// 이 객체에서 타이머를 하나 달아주고 5초뒤에 Explore라는 함수를 실행
// 시간 = ExplosionDelay
// 반복할지 말지 = false
GetWorld()->GetTimerManager().SetTimer(
ExplosionTimerHandle,
this,
&AMineItem::Explode,
ExplosionDelay,
false
);
}
void AMineItem::Explode() {
//액터 검색
//겹친 액터들을 저장해줄 배열 생성
TArray<AActor*> OverlappingActors;
//OverlappingActor들을 전부 가져와주고 넣어주기
ExplosionCollision->GetOverlappingActors(OverlappingActors);
//액터들을 돌면서 태그 확인하기
for (AActor* Actor : OverlappingActors) {
if (Actor && Actor->ActorHasTag("Player")) {
//여기다가 폭발처리 나중에 만들어주기
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Player damaged %f by MineItem"), ExplosionDamage));
}
}
DestroyItem();
}
Q. 미리 선언이 필요한 컴포넌트는 어떻게 알아?
A. 헤더 파일 위에 class USphereComponent;라고 적어두는 것을 C++에서는 전방 선언이라고 부름.
어떤 컴포넌트를 전방 선언해야 하는지 알아내는 기준은 아주 명확하고 간단함.
💡 핵심 기준: "이 변수가 포인터(*)인가?"
헤더 파일(.h) 안에서 변수를 선언할 때, 그 타입이 포인터(*)나 참조(&) 형태라면 무조건 전방 선언을 할 수 있습니다. (그리고 하는 것이 권장됨.)
예시 코드
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
USceneComponent* Scene;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
USphereComponent* Collision;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
UStaticMeshComponent* StaticMesh;
여기서 Scene, Collision, StaticMesh는 모두 끝에 *가 붙어있는 포인터 변수.
따라서 이 세 가지는 모두 헤더 파일 상단에 #include 대신 전방 선언을 쓰는 것이 좋음.
위 코드의 정석적인 형태는 아래와 같이 세 개 모두 전방 선언을 해주는 것.
class USceneComponent;
class USphereComponent;
class UStaticMeshComponent;
UCLASS()
class TESTPROJECT2_API ABaseItem : public AActor, public IItemInterface
// ... (생략)
(참고: USceneComponent나 UStaticMeshComponent를 전방 선언 안 했는데도 에러가 안 났다면, 위쪽에 인클루드 된 GameFramework/Actor.h 내부 어딘가에서 이미 포함되어 있어서 운 좋게 넘어간 것일 뿐. 명시적으로 다 적어주는 것이 좋습니다.)
🛠️ 왜 #include 대신 전방 선언을 쓸까? (성능 이슈 방지)
.h 파일을 읽을 때, USphereComponent*라는 녀석이 메모리를 얼마나 차지할지 계산해야함. 그런데 포인터는 가리키는 대상이 얼마나 크든 상관없이 무조건 8바이트(64비트 기준).USphereComponent의 내부가 어떻게 생겼는지(변수가 몇 개인지, 함수가 뭔지) 당장 .h 파일에서는 알 필요가 없음. "이런 이름의 클래스가 존재한다"는 사실만 알려주면 됨.#include "Components/SphereComponent.h"를 해버리면, 컴파일러는 SphereComponent.h 안에 있는 모든 내용과 그 파일이 참조하는 또 다른 파일들을 수만 줄에 걸쳐 다 긁어와야 함. 전방 선언을 쓰면 이 과정을 .cpp 파일로 미룰 수 있어서 빌드 속도가 빨라짐.📌 요약 가이드: 언제 뭘 써야 할까?
.h)에서:*)로만 가지고 있을 때 👉 전방 선언 (class Uxxx;)public AActor)받거나, 포인터가 아닌 일반 객체로 가지고 있을 때 👉 #include 필수.cpp)에서:Collision->SetSphereRadius(50.f); 등) 👉 #include "Components/SphereComponent.h" 필수🤔 컴포넌트 이름은 어떻게 알 수 있나요?
언리얼 에디터에서 컴포넌트를 추가할 때 보이는 이름(예: Sphere Collision)에 접두사 U를 붙여서 유추하거나, 공식 문서 검색, 또는 라이브 코딩 중 IDE(비주얼 스튜디오 등)의 자동 완성 기능을 활용하면 쉽게 정확한 클래스명(USphereComponent)을 알 수 있음.