no image
[ UE, C++ ] 언리얼 엔진의 라이플 사이클 및 액터 생성
엔진으로 개발을 하다보면 가장 중요한 것이 그 엔진의 라이플사이클이다.객체의 초기화나 오브젝트 할당, 파괴 등의 중요한 역할을 하기 때문이다.그래서 오늘은 언리얼 엔진의 라이플 사이클에 대해 먼저 알아보았다.그리고 관련 함수들을 사용하여 C++로 액터를 동적으로 이동, 회전, 크기변화까지 시켜볼 생각이다. 언리얼엔진의 라이플사이클생성자액터가 생성되기 전에 호출됨.객체 메모리에 생성될 때 1번 호출됨(주로 초기화 작업을 활용)객체에 컴포넌트를 추가할 때? 사용하는 것 같음 PostInitializeComponents()컴포넌트가 완성된 직후 호출월드에 객체가 등장하기 직전에 컴포넌트끼리 상호작용할 때 활용 BeginPlay()월드에 스폰된 직후에 호출됨다른 액터들도 이미 준비되어있어서 자유롭게 상호작용이 ..
2025.01.23
no image
[ UE ] C++ 코드로 RootComponent와 StaticMesh 생성하기
오늘은 C++ 코드로 컴폰넌트들을 다루는 방법에 대하여 공부를 진행했다.처음에 클래스를 생성하고 아무것도 없는 상태에서 월드에 올려보니 내가 배치하고 싶은 위치가 아니라 월드의 중점?에 강제로 배치가 되는 것을 경험했다.. 루트 컴포넌트 추가이건 아직 생성한 클래스에서 좌표에 대한 정보를 알 수 없어서 해당 상황이 생긴다고 한다.그래서 생성한 클래스에 루트 컴포넌트를 붙여줘야하는데, Scene 컴포넌트는 좌표계만 들어있는 깔끔한 상태의 컴포넌트다.그래서 Scene 컴포넌트를 주로 루트 컴포넌트로 설정해준다고 한다.#pragma once#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "Item.generated.h"UCLASS()class..
2025.01.22
[C++ ] C++의 꽃, 다형성에 대한 모든 개념 정리
매번 코드를 짜긴 짰는데.. 구조는 대강 설계할 수 있었으나 코드를 짤 때에는 gpt의 도움을 살짝씩 받았었다.그래서 인터넷 검색없이 코드를 짜라고 하면 헷갈리고 아직 이해가 부족한 부분이 많다고 생각했다.언리얼 엔진 문법을 익히기 전에 C++ 문법부터 제대로 익히고 들어가는게 이해하기 수월하고 맞을 것 같다는 생각이 들어 찐하게 학습을 진행하게 되었다. 이해하면서 정리를 했기 때문에 내가 어떻게 이해했는지와 비전공자들도 이해할 수 있을 정도로 아주아주 자세하게 과정을 적어놓았다.부디 이 글을 읽는 대부분의 사람들이 다형성을 이해하고 돌아가면 좋겠다.  다형성객체지향 프로그래밍의 핵심 개념 중 하나하나의 인터페이스로 다양한 객체를 다룰 수 있다는 특징코드의 재사용성과 확장에 굉장히 용이 가상 함수(vir..
2025.01.21
C++
no image
[ UE ] 언리얼 엔진 프로젝트 구조 및 빌드 방법 (+꿀팁)
오늘은 언리얼 엔진에서 프로젝트를 처음 생성했을 때 만들어지는 폴더 구조와 빌드는 어떻게 하는지 등에 대해서 알아보았다.프로젝트를 처음 생성했을 때 알아두면 좋을 정보들이라고 생각한다. 프로젝트 생성우리는 최대한 깔끔한 상태의 프로젝트를 생성하는 것이 목표이기 때문에 Games에서 Blank를 선택해준다.그리고 C++로 개발을 할거라 C++을 선택해주고, 본인의 데스크탑이나 컴퓨터 성능에 따라 Maximum 또는 Scalable을 선택해준다.그리고 Starter Content는 체크해제시켜준다.  (만약 이 Start Content가 필요하다면 추후에 프로젝트에서 추가하는 방법도 있으니 참고하면 좋을 것 같다.) 이렇게 프로젝트를 생성하면 자동으로 폴더가 생성되는 구조를 볼 수 있는데, 이 폴더들은 뭘 ..
2025.01.20
no image
[ C++ ] Text_RPG 팀플 완료
오늘은 드디어 팀프로젝트로 진행하던 텍스트 게임 프로젝트를 끝냈다.간단한 소감과 배웠던 내용을 기록하려고 한다.  나의 담당파트- 좀비 타입별 무작위 스탯 설정- 팩토리를 사용한 좀비 랜덤 생성- 랜덤 유틸 함수 구현- 프로젝트 아스키 아트 추가 및 전반적인 멘트 통일화  소감전부터 팀플을 몇 번하긴 했었는데 오랜만에 하기도 하고, 낯을 많이 가려서 초반에는 말을 잘 못했던 것 같다.프로젝트 팀원은 컴퓨터공학을 전공한 사람과 비전공자가 섞인 6명으로 진행했다.이렇게 다양한 팀원들이 섞이면서 내가 몰랐던 부분도 배워가고 아는 부분은 또 알려주고, 문법이나 개발에 대한 이해가 더 증가한 것 같다. 팀원들이 다 착하고 열심히 해서 다음 프로젝트에도 같이 하면 좋을 것 같다. 게임 컨셉은 좀비로 진행했다.(사실..
2025.01.17
C++
[C++] 프로그래머스 3진법 뒤집기
내 풀이진법 변경을 까먹어서 처음에 봤을 때 많이 헷갈렸던 문제이다.먼저 주어진 10진수를 3으로 나눈 나머지를 저장해준다.저장된 나머지를 다시 3을 곱해서 10진수로 만들어준다. 풀이 코드#include #include #include #include using namespace std;int solution(int n) { int answer = 0; vector temp; int three = 1; while (n != 0) { temp.push_back(n % 3); n /= 3; } for (int i = temp.size() - 1; i >= 0; i--) { answer += temp[i] * thre..
2025.01.16
[Visual Studio] 한글 깨짐 현상
오늘은 진행하던 팀 프로젝트를 병합하는 과정에서 한글 깨짐 현상이 발생했다.좀비 생성 부분은 게임 매니저와 다 병합했고 손이 남아서 프로젝트 전체 문구와 꾸미기를 담당했다. 아스키 아트라는 것을 알게 되었고, 얘로 꾸미니까 볼품없는 콘솔창이 뭔가 귀여워진 느낌이 들었다. 아스키아트아스키 아트(ASCII Art)는 ASCII(미국 표준 문자 코드)로 표현할 수 있는 문자, 숫자, 기호 등을 조합하여 만든 그림이나 디자인.컴퓨터의 텍스트 환경에서 그림이나 도형을 표현할 수 있는 방법으로 발전해왔고 아스키 아트는 주로 문자 기반 환경에서 이미지나 아이콘을 표현하기 위해 사용된다. /\_/\ ( o.o ) > ^ 간단하게 고양이를 출력해보았다. 귀여워.. 그리고 전체적으로 멘트를 통일화 시켰다. 어디는 ~ ..
2025.01.15
C++
no image
[C++] 진짜 랜덤값? 가짜 랜덤값?
저번 포스팅에서 올렸던 좀비 클래스를 구현하다가 랜덤함수를 검색했는데 진짜 랜덤값 이라는 키워드가 궁금증을 자극해서 알아보게 되었다. 먼저 랜덤값하면 말 그대로 어떤 값이 나올지 모르는, 출력이 되어야만 알 수 있는 그런 숫자라고 생각한다.사실 게임 속에서도 아이템을 획득하거나 뽑기를 할 때 랜덤으로 확률을 정하고 결과를 얻는 부분이 많이 있다.그래서 구현할 때는 모두 랜덤함수를 사용했을거라 생각했던 나... 하지만 랜덤값에도 가짜가 있다는 사!실! 알고있었나요...? 먼저 rand 함수와 srand()는 무엇인지 궁금해졌다.Seed 숫자(=시드)컴퓨터가 난수를 생성할 때 사용하는 특정한 시작 숫자.이 숫자를 기준으로 정해진 알고리즘을 실행하면서 마치 난수처럼 보이는 수열을 생성하는 것 srand()ra..
2025.01.14
C++
no image
[C++] Text RPG 몬스터 생성 로직 설계
간단하게 Text를 통해 RPG 게임을 만드는 팀 프로젝트를 진행 중이다.프로그램을 시작하면 플레이어 생성을 하고 플레이어의 레벨에 맞는 몬스터를 생성한다.그리고 플레이어 또는 몬스터의 체력이 0이 될 때까지 전투를 진행한 뒤 보상을 받고 상점 방문 여부를 받는다.그 후부터는 보스를 처치하거나 플레이어의 체력이 0이 될 때까지 계속 반복하는 흐름이다. 나는 여기서 몬스터 구현을 담당했다.로직을 꼼꼼하게 설계하면 코드 작성이 쉬워지기 때문에 설계에 많은 시간을 쏟았다.아래는 기능의 흐름과 어떤 것들을 구현하면 좋을지 몬스터 생성 관련 로직들을 설계한 내용이다.각 클래스가 가지고 있어야할 멤버 변수와 멤버 함수들을 정의했다.팀 회의를 통해 공통된 특성은 Actor라는 상위 클래스로 정의해서 상속받아 사용하는..
2025.01.13
C++

엔진으로 개발을 하다보면 가장 중요한 것이 그 엔진의 라이플사이클이다.

객체의 초기화나 오브젝트 할당, 파괴 등의 중요한 역할을 하기 때문이다.

그래서 오늘은 언리얼 엔진의 라이플 사이클에 대해 먼저 알아보았다.

그리고 관련 함수들을 사용하여 C++로 액터를 동적으로 이동, 회전, 크기변화까지 시켜볼 생각이다.

 

언리얼엔진의 라이플사이클

생성자

  • 액터가 생성되기 전에 호출됨.
  • 객체 메모리에 생성될 때 1번 호출됨(주로 초기화 작업을 활용)
  • 객체에 컴포넌트를 추가할 때? 사용하는 것 같음

 

PostInitializeComponents()

  • 컴포넌트가 완성된 직후 호출
  • 월드에 객체가 등장하기 직전에 컴포넌트끼리 상호작용할 때 활용

 

BeginPlay()

  • 월드에 스폰된 직후에 호출됨
  • 다른 액터들도 이미 준비되어있어서 자유롭게 상호작용이 가능

 

Tick(float DeltaTime)

  • 매 프레임마다 호출됨(무거운 로직을 담으면 성능이 떨어질 수도 있기 때문에 조심해서 사용)
  • 캐릭터 이동, 물리 계산 같은 부분들에 활용

 

Destroyed()

  • 삭제되기 직전에 호출됨
  • 보통 리소스 정리같은 부분을 담당

 

Endplay(const EEndPlayReason::Type EndPlayReason )

  • 게임 종료의 상황에서 호출
  • 이 함수는 Enum 타입으로, 어떤 이유로 EndPlay 함수가 호출되었는지 구분할 수 있다고 한다!

 

한줄요약(결론)

생성자() -> PostInitializeComponents() -> BeginPlay() -> (TIck()) -> Destory() -> EndPlay() 순으로 호출된다.

 

+Desrtory와 EndPlay()의 차이

(언리얼 초심자는 이 두 함수를 굉장히 혼란스러워한다고 하길래 나만의 이해팁을 공유하고자 한다)

 

내가 이해하기로는 Destroy()는 크롬창의 각 탭에 달려있는 x의 느낌으로 이해했다.

 

근데 이제 EndPlay()는 크롬창의 전체닫기 같은 느낌으로 이해했다.

 

대강 요런 느낌??

 

그리고 일단 각 함수들이 언제 호출되는지 좀 명확하게 보고싶어서 언리얼 엔진의 로그시스템을 활용해주었다.

먼저 플레이 버튼을 누른 직후이다.

이렇게 생성자(Constructor) -> PostInitializeComponents -> BeginPlay 순으로 로그가 찍히는 것을 볼 수 있다.

 

 

플레이를 종료했을 때

플레이 종료를 하면 EndPlay가 호출되는 것을 볼 수 있다.

 

액터를 삭제했을 때

플레이 도중에 액터를 삭제해보았다. 그러니까 바로 Destory 함수가 호출이 된 것을 볼 수 있었다!

 

이제 이 액터를 C++ 코드로 Transform을 건드려볼려고 한다.

 

C++로 액체 Transform 동적으로 변화하게하기

플레이를 눌렀을 때, 위로 이동하면서 회전도 하고 크기도 계속 변화하는 별을 만드려고 했다.

그래서 일단은 위치지정을 해줬고, 그 다음에 이동값과 회전값 등을 더하는 형식으로 진행했다.

크기 변화는 초기크기와 최대크기를 지정해줬고, 특정 시간에 맞춰서 크기 변화를 계속 시켜줬다.

 

사용한 코드

#include "Item.h"


AItem::AItem()
{
	// 씬 컴포넌트를 루트로 지정하는 과정
	SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
	SetRootComponent(SceneRoot);

	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMeshComp->SetupAttachment(SceneRoot);
	

	static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Star_C.SM_Star_C"));
	if (MeshAsset.Succeeded())
	{
		StaticMeshComp->SetStaticMesh(MeshAsset.Object);
	}

	static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Power_Sphere_Inst.M_Power_Sphere_Inst"));
	if (MaterialAsset.Succeeded())
	{
		StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
	}

	PrimaryActorTick.bCanEverTick = true;

	RotationSpeed = 90.0f;
	SpeedZ = 200.0f;
	TimeAccumulator = 0.0f;
	ChangeScaleTime = 2.0f;
	MaxScale = 5.0f;

}

void AItem::BeginPlay()
{
	Super::BeginPlay();
	//SetActorLocation(FVector(300.0f, 200.0f, 100.0f));
	//SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));  // FRoatator의 형태 - pitch(y축), yaw(z축), roll(x축) 순
	//SetActorScale3D(FVector(2.0f));

	FVector NewLocation(300.0f, 200.0f, 100.0f);
	FRotator NewRotation(0.0f, 90.0f, 0.0f);
	FVector NewScale(2.0f);

	InitialScale = NewScale;

	FTransform NewTransform(NewRotation, NewLocation, NewScale);

	SetActorTransform(NewTransform);

	

}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 부동소수점은 오차가 굉장히 크기 때문에 불필요한 0.00000001에서도 회전을 한다.
	// 그래서 언리얼에서 제공하는 FMath의 0에 근접한지 계산하는 함수를 사용해준다.
	if (!FMath::IsNearlyZero(RotationSpeed))
	{
		AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
	}
	
    AddActorLocalOffset(FVector(0.0f, 0.0f, SpeedZ * DeltaTime));
	TimeAccumulator += DeltaTime;

	if (TimeAccumulator > ChangeScaleTime)
		TimeAccumulator -= ChangeScaleTime;

	float ScaleFactor = InitialScale.X + MaxScale * FMath::Sin((2 * PI / ChangeScaleTime) * TimeAccumulator);

	SetActorScale3D(FVector(ScaleFactor, ScaleFactor, ScaleFactor));
		
}

언리얼에서 제공해주는 유용한 함수가 많아 신기했다. 특히 FMath 클래스의 Sin 함수에서 되게 간편하게 구현할 수 있다는 느낌을 받았다.

 

결과

어디까지 올라가니..?

아주 그냥 잘 날아다니고, 제일 왕 큰 별이 되어라 내 스타야...

잘 보이진 않지만 자세히보면 별이 회전하는 부분도 보인다..! ㅎㅎ

 

마지막 말

아직 언리얼 문법에 익숙치 않아 스케일 변화할 때는 gpt의 도움을 살짝 받았다..

메서드 이름에 집착하지말고 전체적인 개발 흐름에 대한 느낌을 익혀야할 것 같다...

천천히 조금씩 꾸준히하면 무엇이든 성장할 것이다!!!! 파이팅~~!!!!

 

오늘은 C++ 코드로 컴폰넌트들을 다루는 방법에 대하여 공부를 진행했다.

처음에 클래스를 생성하고 아무것도 없는 상태에서 월드에 올려보니 내가 배치하고 싶은 위치가 아니라 월드의 중점?에 강제로 배치가 되는 것을 경험했다..

 

루트 컴포넌트 추가

이건 아직 생성한 클래스에서 좌표에 대한 정보를 알 수 없어서 해당 상황이 생긴다고 한다.

그래서 생성한 클래스에 루트 컴포넌트를 붙여줘야하는데, Scene 컴포넌트는 좌표계만 들어있는 깔끔한 상태의 컴포넌트다.

그래서 Scene 컴포넌트를 주로 루트 컴포넌트로 설정해준다고 한다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class PRACTICE_API AItem : public AActor
{
	GENERATED_BODY()
	
public:	
	AItem();

protected:
	USceneComponent* SceneRoot;  // 포인터 타입의 멤버변수
};

 

이렇게 Scene Component를 가져올 수 있는 멤버 변수를 하나 생성해줬고,

 

#include "Item.h"

AItem::AItem()
{
	// 씬 컴포넌트를 루트로 지정하는 과정
	SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
	SetRootComponent(SceneRoot);

}

이렇게 나는 "SceneRoot" 라는 이름으로 생성하고 싶어서 TEXT 안에 이름을 넣어줬다.(이 부분은 원하는 이름을 넣어줘도 된다)

 

이렇게 좌표계가 있는 Scene 컴포넌트를 붙인 상태에서 다시 클래스를 월드에 올려보면 내가 지정한 위치에 잘 올라가는 것을 확인할 수 있다!

 

 

StaticMesh와 Material 추가

하지만.. 실제로 눈에 보이는 것들이 없어서 어디있는지 Outliner를 통해서만 보인다

뭔가 형태를 붙여주고 싶어서 StaticMesh와 Material을 붙여주었다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class PRACTICE_API AItem : public AActor
{
	GENERATED_BODY()
	
public:	
	AItem();

protected:
	USceneComponent* SceneRoot;  // 포인터 타입의 멤버변수
	UStaticMeshComponent* StaticMeshComp;

};
#include "Item.h"

AItem::AItem()
{
	// 씬 컴포넌트를 루트로 지정하는 과정
	SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
	SetRootComponent(SceneRoot);

	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMeshComp->SetupAttachment(SceneRoot);
	

	static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Star_C.SM_Star_C"));
	if (MeshAsset.Succeeded())
	{
		StaticMeshComp->SetStaticMesh(MeshAsset.Object);
	}

	static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Power_Sphere_Inst.M_Power_Sphere_Inst"));
	if (MaterialAsset.Succeeded())
	{
		StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
	}

}

이렇게 안전하게 해당 오브젝트들이 잘 있는지 확인을 한 다음에 적용하는 방식으로 해줬다.

아! 그리고 Static Mesh 나 Material의 경로를 가져오는 방법은 에디터에서 원하는 Mesh를 오른쪽 마우스 클릭 -> CopyReference 해서 가져오면 된다.

 

가져오게 된다면

/Script/Engine.MaterialInstanceConstant'/Game/Resources/Materials/M_Banana_B.M_Banana_B'

이런 형태일텐데 앞부분은 그냥 다 자르고

'/Game/Resources/Materials/M_Banana_B.M_Banana_B'

이렇게 경로 부분만 사용하면 된다.

 

아 참고로 나는 여기서 헷갈렸던 부분이 Game 폴더가 어디서 튀어나왔는지 ? 의문이 들었었는데

Content 폴더 = Game 폴더 라고 생각하면 된다!

 

 

결과

 

짜잔~ 처음에 아무것도 붙이지 않은 클래스를 올렸을 때에는 이미지에 표시되는 원점으로 강제 이동했지만,

좌표 정보를 담고 있는 Scene 컴포넌트를 루트 컴포넌트로 설정해주고,

그 아래에 별모양의 Static Mesh와 노란색의 Material을 붙여주니 원하는 위치에, 눈에 보이는 형태로 월드에 배치된 것을 볼 수 있다!

 

 

트러블슈팅

문제상황 1. 비주얼 스튜디오 솔루션 연결 문제

- 중간에 언리얼 에디터에서 C++ Classes 라는 폴더가 안보여서(Actor 클래스 만들었음) 클래스 삭제하고 캐시 폴더 삭제하고 이렇게 이것저것 건드리다가 비주얼스튜디오 솔루션에서 언리얼 엔진을 찾을 수 없다는 문구가 떴다..

원래는 위의 이미지 대로 떠야하는데 Engine 폴더 밑의 UE5와 내 Practice 프로젝트 부분에서 "UE5 솔루션을 찾을 수 없음", "파일을 찾을 수 없음" 이라고 자세히 기억은 안나지만.. 오류가 났었다.

 

해결한 방법

비주얼스튜디오를 차분하게 닫고, 에디터를 실행해서 Tools -> Refresh Visual Studio 2022 Project를 클릭 -> Open Visual Studio 2022를 클릭했더니 다시 연결이 되었다ㅎㅎ (바보같이 비주얼스튜디오 안끄고 계속 리프레쉬 누른 사람.. 저요..)

 

문제 상황 2. 비주얼스튜디오에서 디버그를 할 때 언리얼 에디터가 아닌 window 콘솔창이 뜨는 문제

- 아마도 위에 상황에서 이것저것 캐시파일이나 저장했던 파일들을 삭제하는 과정에서 일어났을 것이다.

 

해결한 방법

나 같은 경우엔 내 프로젝트(위의 이미지에서 Games/Pratice)에 마우스 우클릭 -> 시작프로젝트로 설정으로 하고 나니까 다시 정상적으로 언리얼 에디터가 작동했다.

내 시작 프로그램이 UnrealBuildTool 이었나..? 걔로 잡혀서 발생하는 오류였다.

 

매번 코드를 짜긴 짰는데.. 구조는 대강 설계할 수 있었으나 코드를 짤 때에는 gpt의 도움을 살짝씩 받았었다.

그래서 인터넷 검색없이 코드를 짜라고 하면 헷갈리고 아직 이해가 부족한 부분이 많다고 생각했다.

언리얼 엔진 문법을 익히기 전에 C++ 문법부터 제대로 익히고 들어가는게 이해하기 수월하고 맞을 것 같다는 생각이 들어 찐하게 학습을 진행하게 되었다.

 

이해하면서 정리를 했기 때문에 내가 어떻게 이해했는지와 비전공자들도 이해할 수 있을 정도로 아주아주 자세하게 과정을 적어놓았다.

부디 이 글을 읽는 대부분의 사람들이 다형성을 이해하고 돌아가면 좋겠다.

 

 

다형성
  • 객체지향 프로그래밍의 핵심 개념 중 하나
  • 하나의 인터페이스로 다양한 객체를 다룰 수 있다는 특징
  • 코드의 재사용성과 확장에 굉장히 용이

 

가상 함수(virtual)
  • 부모 클래스에서 선언된 함수이지만 파생 클래스에서 재정의(override)할 수 있음
    • 파생 클래스에서 정의해도 되고, 안 해도 됨
  • 파생 클래스에서 재정의를 하게 된다면 파생 클래스에서 재정의한 함수가 호출
  • 재정의 하지 않았다면 부모 클래스의 기본 구현된 함수가 호출

 

가상 함수 선언 문법
virtual void makeSound() const;  // 가상 함수 선언

// 파생 클래스에서 재정의하는 것은 선택

 

순수 가상 함수(virtual)
  • 파생 클래스에서 반드시 재정의해야 하는 멤버 함수
  • 재정의하지 않으면 사용할 수 없음(컴파일 에러 발생)
  • 다형성을 활용하여 파생 클래스마다 반드시 다른 동작을 구현하도록 강제

 

순수 가상 함수 선언 문법
virtual void makeSound() const = 0;  // 순수 가상 함수 선언

// 반드시 파생 클래스에서 makeSound를 재정의해야만 사용할 수 있음

(자세한 예시는 추상 클래스 예제 코드와 함께)

 

추상클래스(abstract class)
  • 특별히 기본 클래스로 사용되도록 설계된 클래스
  • 하나 이상의 순수 가상 함수가 포함되어 있음
    • 추상클래스로 정의하고 싶다면 멤버 변수 없이 순수 가상 함수로만 이뤄진 클래스 형태가 가장 바람직
  • 객체를 자체적으로 생성할 수 없고 반드시 상속을 거쳐야만 사용 가능(아래에서 자세히 설명)
    • 상속받은 파생 클래스 또한 반드시 부모의 순수 가상 함수를 override 해줘야 됨

 

객체 생성이 안되는 예제 코드

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal() {};
    ~Animal() {};
    virtual void makeSound() const = 0;  // 순수 가상 함수
    //virtual void makeSound() const {};  // 가상 함수
};

class Cat : public Animal
{
};

class Dog
{
public:
    void makeSound()
    {
        cout << "Animal을 상속받지 않은 Dog 입니다." << endl;
    }
};

int main()
{
    Animal animal;  // 에러
    Cat cat;  // 에러
    Dog dog;

    /*animal.makeSound();
    cat.makeSound();
    dog.makeSound();*/

}

이유 : Animal 클래스에는 makeSound라는 순수 가상 함수가 선언되어있음.

즉, Animal 클래스는 추상 클래스가 됨. 따라서 Animal 클래스 자체적으로는 객체 생성이 불가능함

Cat 클래스는 Animal을 상속받는 자식 클래스가 되는데, 순수 가상 함수는 상속받은 객체에서 무조건 재정의해야하지만 Cat 클래스에서는 순수 가상 함수인 makeSound를 재정의하지 않아 객체 선언 불가능함.

 

그럼 이 코드가 정상적으로 동작할 수 있게 하는 방법은?

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal() {};
    ~Animal() {};
    virtual void makeSound() const = 0;  // 순수 가상 함수
    //virtual void makeSound() const {};  // 가상 함수
};

class Cat : public Animal
{
public:
    void makeSound() const override
    {
        cout << "Animal을 상속받았고, Cat에서 makeSound 순수가상함수 overriding 했음." << endl;
    }
};

class Dog
{
public:
    void makeSound()
    {
        cout << "Animal을 상속받지 않은 Dog 입니다." << endl;
    }
};

int main()
{
    //Animal animal;
    Cat cat;
    Dog dog;

    //animal.makeSound();
    cat.makeSound();
    dog.makeSound();

}

Cat에서 상속받은 추상 클래스 Animal의 순수가상함수 makeSound를 override으로 재정의 해줌으로써 객체 선언이 가능해짐.

결과

 

부모 클래스 포인터
  • 다형성을 구현하기 위해서 부모 클래스 타입의 포인터를 사용함
  • 이를 통해 해당 부모 클래스를 상속받는 다른 파생 클래스에도 쉽고 유연하게 접근 가능

 

부모 클래스 포인터 타입으로 객체를 관리하는 예제 코드 1 (다형성 활용)

Animal* animals[3];
animals[0] = new Cat();
animals[1] = new Dog();
animals[2] = new Bird();

for (int i = 0; i < 3; i++) {
    animals[i]->makeSound();  // 각 클래스의 makeSound()가 호출됨.
    delete animals[i];        // 동적 할당된 메모리 해제.
}

 

부모 클래스 포인터 타입으로 객체를 관리하는 예제 코드 2 (다형성 활용)

Animal* cat = new Cat();
cat->makeSound();  // Cat 클래스의 makeSound() 호출.

Animal* dog = new Dog();
dog->makeSound();  // Dog 클래스의 makeSound() 호출.

 

이렇게 부모 클래스 포인터 타입으로 하면 좋은 이유
Animal* animal = new Cat();  // Animal 포인터로 Cat 객체 다룸.
animal->makeSound();         // Cat의 makeSound()가 호출됨.

animal = new Dog();          // Animal 포인터로 Dog 객체 다룸.
animal->makeSound();         // Dog의 makeSound()가 호출됨.

  1. 부모 클래스인 Animal 타입의 animal 포인터 하나로 Animal을 상속받고 있는 파생 클래스에 접근하기 유용
  2. 메모리 낭비 최소화할 수 있음

 

부모 클래스 포인터 타입으로 객체를 관리하지 않는 예제 코드(다형성 활용 x)

Cat* cat = new Cat();
Dog* dog = new Dog();

cat->makeSound();  // Cat의 메서드 호출.
dog->makeSound();  // Dog의 메서드 호출.

delete cat;
delete dog;

 

인터페이스(Interface)
  • 순수 가상 함수로만 이뤄진 추상 클래스를 인터페이스라고 칭함
  • 인터페이스는 구현이 아닌 설계(구조)만 제공
  • 멤버 변수를 가지지 않으며 오로지 메서드만 정의
  • 다중 상속 가능(순수 가상 함수만 포함되어있으므로 충돌 가능성이 낮기 때문)

 

 

마지막 말

이렇게 큰 틀은 다형성이지만 그 안에 우리가 개발하면서 필요한 여러 지식들이 들어가 있는 모습을 볼 수 있다.

다형성을 사용하면 특히 메스드의 이름 같은 부분을 절약할 수 있어서 좋고, 다양하게 재정의하는 부분이 재밌게 느껴지는 것 같다.

'C++' 카테고리의 다른 글

[ C++ ] 프로그래머스 비밀지도  (0) 2025.04.25
[UE, C++] 캐릭터 움직여보기  (0) 2025.02.03
[ C++ ] Text_RPG 팀플 완료  (1) 2025.01.17
[Visual Studio] 한글 깨짐 현상  (0) 2025.01.15
[C++] 진짜 랜덤값? 가짜 랜덤값?  (0) 2025.01.14

 

오늘은 언리얼 엔진에서 프로젝트를 처음 생성했을 때 만들어지는 폴더 구조와 빌드는 어떻게 하는지 등에 대해서 알아보았다.

프로젝트를 처음 생성했을 때 알아두면 좋을 정보들이라고 생각한다.

 

프로젝트 생성

우리는 최대한 깔끔한 상태의 프로젝트를 생성하는 것이 목표이기 때문에 Games에서 Blank를 선택해준다.

그리고 C++로 개발을 할거라 C++을 선택해주고, 본인의 데스크탑이나 컴퓨터 성능에 따라 Maximum 또는 Scalable을 선택해준다.

그리고 Starter Content는 체크해제시켜준다.  (만약 이 Start Content가 필요하다면 추후에 프로젝트에서 추가하는 방법도 있으니 참고하면 좋을 것 같다.)

 

이렇게 프로젝트를 생성하면 자동으로 폴더가 생성되는 구조를 볼 수 있는데, 이 폴더들은 뭘 저장하는지 알아볼 것이다.

 

 

프로젝트 전체 폴더 구조(실제구조)

- Source 폴더 : 작성한 스크립트들 저장

- Content 폴더 : 블루프린트, 에셋들을 저장

- sln 파일 : 비주얼 스튜디오 솔루션 파일

- .uproject 파일 : 언리얼 에디터 파일

- Binaries 파일 : 빌드했을 때 실행파일, dll 파일 등 저장

- Config 폴더 : 설정 파일들 저장

- DerivedDataCache 폴더 : 캐시 데이터 저장

- Intermediate 폴더 : 중간에 빌드 임시 파일 저장

- Saved 폴더 : 스크린샷이나 로그 등을 저장

 

 

솔루션의 폴더 구조와 프로젝트 폴더 구조가 다른 이유?

그런데 솔루션을 열어보면 비주얼 스튜디오 상에서는 또 폴더 구조들이 조금 다른게 보일 것이다.

그 이유는 비주얼스튜디오에서 빌드를 편하게 할려고 일종의 가상의 구조로 재구성한 것이라 그렇다고 한다.

 

 

sln 상에서의 폴더 구조

 

- Engine 폴더 : 언리얼 엔진의 코어 코드가 들어있음(엔진의 코어 기능을 커스텀하고 싶을 때 건드림)

- Games 폴더 : 실제 개발자들이 개발을 하는 폴더

- Programs 폴더 : 게임 외 서버나 유틸리티 프로그램들이 담겨있음

- Rules 폴더 : 게임 모듈들의 빌드 규칙을 담아놓음

- Visualizers 폴더 : 디버깅할 때 언리얼의 자료구조를 편하게 하기 위한 파일 설정들이 저장되어있음

 

선택되어있는 부분이 프로젝트의 루트 폴더라고 생각하면 된다.

 

 

선택된 폴더에서 대부분의 개발작업을 진행하게 된다.

 

 

Unreal에서의 빌드 프로세스

Unreal 엔진에서 빌드를 할 때의 진행 과정은 다음과 같다.

c 파일 수정 → 빌드(컴파일 + 링크) → DLL 파일(빌드를 한 결과물이 저장) 생성 → 언리얼 에디터

 

 

다음으로 비주얼 스튜디오 상단에 디버깅을 할 수 있는 모드가 많은데 다 어떤 역할들을 하는걸까?

 

 

디버깅모드

- DebugGame : 디버그에 대해 최적화된 모드(디버그에 대한 모든 것들을 가지고 좀 세부적으로, 최적으로 디버깅이 됨. 대신 좀 느림)

- Development : 가장 일반적인 빌드 모드. 빨리 개발하고 테스트하고 할 때 선택. DebugGame보다는 모듈이 많이 축소되어있지만 대신 속도가 빠름. 그래서 가벼운 테스트용으로 적합(대부분의 디버깅은 Development Editor를 채택)

- Shipping : 빌드, 최종 릴리즈 단계에서 선택, 디버그 정보가 모두 빠져있음. 게임 출시 직전 테스트하는 용도

 

여기서 궁금한 것이 하나 생겼다.

DebugGame이라는 키워드가 붙은 모드는 DebugGame, DebugGame Editor인데 뭐가 다른걸까?

 

 DebugGame과 DebugGame Editor의 차이?

DebugGame 모드로 설정한 뒤 빌드를 하면 완전히 분리된 실행파일 형태에서 디버깅을 할 수 있다.

DebugGame Editor로 설정한 뒤 빌드를 하게 되면 Unreal 에디터에서 바로 디버깅을 할 수 있는 차이가 있다. 

(Development와 Development Editor도 위의 두 모드의 차이점과 유사하다고 생각하면 좋을 것 같다!)

 

 

디버깅 모드까지 살펴봤으니 이제 빌드하는 과정을 알아보자.

보통 비주얼 스튜디오에서 빌드를 생각한다면 솔루션 빌드(ctrl + shift + b)를 많이 생각할 것이다.

이렇게 빌드를 하면 프로젝트에 들어있는 모든 파일들을 빌드하게 된다.

 

프로젝트 전체 빌드의 문제점

사실 프로젝트를 처음 생성한 상태라면 이렇게 전체 빌드를 해줘야 하지만, 코드 1줄만 수정했는데 전체 빌드를 하게 된다면 불필요하게 시간도 많이 소요되고 자원도 많이 소모될 것이다.

 

따라서 이번에 부분 빌드라는 것을 알게되어 공유하고자 한다.

 

프로젝트 부분 빌드

솔루션 우클릭 → 빌드 이렇게 하면 해당 솔루션 파일만 빌드된다.

이렇게 부분적으로 빌드를 하게되면 수정된 파일만 재빌드되기 때문에 시간도 아끼고 효율적이라 부분빌드를 권장하는 편이라고 한다.

 

그래서 얼마나 더 빠른데?

하지만! 전체빌드도 초기 빌드만 느릴 뿐, 2번째 빌드부터는 굉장히 스마트해진 컴파일러가 변경된 파일만 감지해서 빌드하기 때문에 원래 사용하던 전체 빌드의 효율도 나쁘지는 않다.

그렇다고 해도 다른 부분을 끌어와서 빌드하기 때문에 시간은 부분빌드가 더 빠르고 자원도 더 절약된다.

 

아 그리고 빌드를 할 때 몇가지 주의사항이 있다.

 

Unreal 에디터 켜놓은 상태에서 빌드하면 안되는 이유

Unreal 에디터가 빌드될 때 DLL 파일이 갱신되고 에디터가 갱신된 DLL을 가져가는 구조로 진행된다.

하지만 에디터가 켜져있으면 DLL을 붙잡고 있는 상태가 되는데 이 상태에서 빌드를 하게 되면 Unreal이 DLL을 꽉 붙잡고 안놔주기 때문에 ㅎㅎ DLL 파일이 정상적으로 갱신이 되어있지 않은 경우가 발생할 수도 있다.

 

따라서 빌드를 할 때 에러가 떴다면 에디터를 끄고 다시 빌드를 하는게 깔끔하게 될 것이다.

 

그리고 권장사항은 아니지만 꿀팁? 같은 내용을 이번에 알게되어서 공유해보고자 한다.

 

Unreal 프로젝트 안정적이고 멋지게 여는 방법

보통 프로젝트를 열 때 런처에서 다음과 같이 열 것이다.(본인도 이 방법을 알기전까지는 이렇게 열었었음)

2탄으로 돌아올게요..

 

[ C++ ] Text_RPG 팀플 완료

닿메_dahme
|2025. 1. 17. 00:17

오늘은 드디어 팀프로젝트로 진행하던 텍스트 게임 프로젝트를 끝냈다.

간단한 소감과 배웠던 내용을 기록하려고 한다.

 

 

나의 담당파트

- 좀비 타입별 무작위 스탯 설정

- 팩토리를 사용한 좀비 랜덤 생성

- 랜덤 유틸 함수 구현

- 프로젝트 아스키 아트 추가 및 전반적인 멘트 통일화

 

 

소감

전부터 팀플을 몇 번하긴 했었는데 오랜만에 하기도 하고, 낯을 많이 가려서 초반에는 말을 잘 못했던 것 같다.

프로젝트 팀원은 컴퓨터공학을 전공한 사람과 비전공자가 섞인 6명으로 진행했다.

이렇게 다양한 팀원들이 섞이면서 내가 몰랐던 부분도 배워가고 아는 부분은 또 알려주고, 문법이나 개발에 대한 이해가 더 증가한 것 같다. 팀원들이 다 착하고 열심히 해서 다음 프로젝트에도 같이 하면 좋을 것 같다.

 

게임 컨셉은 좀비로 진행했다.(사실 내가 좀비를 너무 좋아하고,, 좀보이드도 너무 재밌게해서 적극적으로 의견을 냈다.)

 

초반에 개발 구조를 어떻게 잡을지를 확실하게 정해야했는데, 다들 경험이 부족하다보니 머리부터 박고 시작해서 중간에 조금 난황을 겪었다.

그리고 필수 기능까지 빠르게하고 그 다음에 도전을 하던지 하자 라는 방향으로 흘러갔는데, 중간에 튜터님의 피드백이 있기도 했고... 나도 작업하다가 더 재밌어져서 필수/도전 외에 더 추가적인 기능이 있으면 좋을 것 같다는 생각을 했었다. 

다른 팀원분들도 사실 그렇게 생각했을지도?

왜냐면 추가기능에 대한 회의를 하면서 아이디어를 모았는데 다들 너무 재밌고 참신한 아이디어를 마구마구 내주셔서 실력만 있다면 그걸 전부 다 구현해보고 싶을 정도였다.

 

프로젝트가 진행되면서 다들 낯도 많이 풀리고, 회의도 많이하고, 아이디어도 많이내서 프로젝트가 점점 맛있어져갔다!(뿌듯)

중간에 깃 충돌해결과 머지하는데 살짝 오류가 나긴 했으나,, 회사 다닐 때 배웠던 깃 다루는 법을 사용해서 큰 문제없이 해결했다.(이것도 뿌듯)

 

그리고 깃 머지하는 과정에서 다른 팀원이 풀리퀘스트를 이용하는 방법으로 머지를 하자고 했다.

나도 뿔리퀘를 사용하는걸 선호하지만..(회사다닐 때, 뿔리퀘로 대부분의 코드를 올렸고,, 그때는 경험이 많은 팀장님께서 코드를 봐주셨기 때문에..) 그 때 상황은 각자 기능 개발하느라 코드리뷰할 여유가 없기도 했고 뿔리퀘로 올리면 레포지토리 주인장이 귀찮아지기 때문에...  그냥 깃 데스크탑에서 간단하게 머지하는 방법을 살짝..? 알려줬다..

 

그래도 팀원들 중에서도 깃을 다뤄보신 분들이 계서서 좀 수월했던 것 같다.

 

프로젝트 기간은 일주일 밖에 안되지만, 하루에 많은 시간을 할애하기도 했고 몰입했던터라 짧은 기간이지만 많은 내용을 배워가는 것 같다.

특히 협업과정을 경험한건 진짜 귀중했던 시간이라고 생각한다.

다른 개발자 분들의 설계나 코드 작성 스타일, 일정 관리, 담당자들 간의 소통 등 진짜 배워가는 점이 많은 프로젝트였다.

 

 

 

배운 내용

- 함수 오버라이딩

- 유니크 포인터 사용 방법

- 코드 병합

 

 

트러블슈팅

- 비주얼 스튜디오, 깃헙 한글 인코딩 문제

 

내 풀이

진법 변경을 까먹어서 처음에 봤을 때 많이 헷갈렸던 문제이다.
먼저 주어진 10진수를 3으로 나눈 나머지를 저장해준다.
저장된 나머지를 다시 3을 곱해서 10진수로 만들어준다.

 

풀이 코드

#include <string>
#include <vector>
#include <iostream>
#include <cmath>

using namespace std;

int solution(int n) {
    int answer = 0;
    vector<int> temp;
    int three = 1;
    
    while (n != 0)
    {
        temp.push_back(n % 3);
        n /= 3; 
    }
 
    for (int i = temp.size() - 1; i >= 0; i--)
    {
        answer += temp[i] * three;    
        three *= 3;
    }
    
    return answer;
}

[Visual Studio] 한글 깨짐 현상

닿메_dahme
|2025. 1. 15. 22:15

오늘은 진행하던 팀 프로젝트를 병합하는 과정에서 한글 깨짐 현상이 발생했다.

좀비 생성 부분은 게임 매니저와 다 병합했고 손이 남아서 프로젝트 전체 문구와 꾸미기를 담당했다.

 

아스키 아트라는 것을 알게 되었고, 얘로 꾸미니까 볼품없는 콘솔창이 뭔가 귀여워진 느낌이 들었다.

 

아스키아트

아스키 아트(ASCII Art)는 ASCII(미국 표준 문자 코드)로 표현할 수 있는 문자, 숫자, 기호 등을 조합하여 만든 그림이나 디자인.
컴퓨터의 텍스트 환경에서 그림이나 도형을 표현할 수 있는 방법으로 발전해왔고 아스키 아트는 주로 문자 기반 환경에서 이미지나 아이콘을 표현하기 위해 사용된다.

 /\_/\  
( o.o ) 
 > ^ <

간단하게 고양이를 출력해보았다. 귀여워..

 

그리고 전체적으로 멘트를 통일화 시켰다. 어디는 ~ 했다.로 출력되고 또 어디는 ~ 했음. 이렇게 출력이 되어서 뒤죽박죽인 느낌이 들었었다.

 

// 작성 중

 

[C++] 진짜 랜덤값? 가짜 랜덤값?

닿메_dahme
|2025. 1. 14. 21:49

저번 포스팅에서 올렸던 좀비 클래스를 구현하다가 랜덤함수를 검색했는데 진짜 랜덤값 이라는 키워드가 궁금증을 자극해서 알아보게 되었다.

 

먼저 랜덤값하면 말 그대로 어떤 값이 나올지 모르는, 출력이 되어야만 알 수 있는 그런 숫자라고 생각한다.

사실 게임 속에서도 아이템을 획득하거나 뽑기를 할 때 랜덤으로 확률을 정하고 결과를 얻는 부분이 많이 있다.

그래서 구현할 때는 모두 랜덤함수를 사용했을거라 생각했던 나...

 

하지만 랜덤값에도 가짜가 있다는 사!실! 알고있었나요...?

 

먼저 rand 함수와 srand()는 무엇인지 궁금해졌다.

Seed 숫자(=시드)
  • 컴퓨터가 난수를 생성할 때 사용하는 특정한 시작 숫자.
  • 이 숫자를 기준으로 정해진 알고리즘을 실행하면서 마치 난수처럼 보이는 수열을 생성하는 것

 

srand()
  • rand 함수에서 사용될 수를 초기화하는 역할
  • 매개변수로 seed 값을 받아서 초기화
  • 정해진 값을 넣게되면 rand 값도 항상 정해지기 때문에 랜덤값이 아님
    • 즉 항상 변하는 값을 넣어줘야하는데 사람들이 생각해낸 것이 바로 시간값

 

rand() 함수 특징
  • 랜덤하게 생성되는 숫자의 크기가 최대 32767까지
  • 생성되는 숫자의 분포가 균일하지 않다
  • 알고리즘이 있기 때문에 설정한 Seed 숫자만 알면 어떤 숫자가 랜덤으로 생성되는지 알 수 있다
  • 프로그램이 생성될 때 값이 정해지기 때문에 여러 번 실행시켜도 동일한 값이 나온다
  • 시간(time)으로 시드 생성(C 스타일)

 

랜덤값 테스트

과연 가짜 랜덤값이 무엇인지 확인해보기 위해 랜덤으로 숫자를 추출하는 간단한 코드로 테스트를 진행해보았다.

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;
int main()
{
    time_t now = time(0);
    tm localTime;

    localtime_s(&localTime, &now);

    cout << "현재 시간: ";
    cout << localTime.tm_hour << ":"
        << localTime.tm_min << ":"
        << localTime.tm_sec << endl;

    for (int i = 0; i < 10; ++i) {
        int num = rand() % 10;
        cout << num << endl;
    }

    return 0;
}

 

결과는??

 

1, 7, 4, 0, 9, 4, 8, 8, 2, 4가 나왔다.

 

자 그럼 다시 한 번 실행해보면 랜덤으로 값이 나오는거니까 또 다른 10개의 값이 출력되겠지?

으잉? 이게 무슨 일이야 같은 값이 나온다.

이유는 처음 실행할 때 난수를 지정하고 그게 저장이 되어서 다음 실행을 해도 똑같은 값이 나오기 때문이다.

 

이걸 해결하기 위해 rand로 값을 반환하기 전에 strand에 시드값으로 항상 다른 값이 나오는 시간을 넣어준다.

기존 작성한 코드에 srand(time(nullptr)); 한 줄을 추가해준다.

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;
int main()
{
    time_t now = time(0);
    tm localTime;

    localtime_s(&localTime, &now);

    cout << "현재 시간: ";
    cout << localTime.tm_hour << ":"
        << localTime.tm_min << ":"
        << localTime.tm_sec << endl;

    srand(time(nullptr));
    for (int i = 0; i < 10; ++i) {
        int num = rand() % 10;
        cout << num << endl;
    }

    return 0;
}

 

결과는?

 

 

이렇게 시간값을 시드로 넣어주니 항상 값이 변하는 것을 볼 수 있다.

하지만 rand 함수는 정해진 알고리즘이 실행되면서 값을 반환하기 때문에 시드값이 어떤 것이 들어갔는지 알아낼 수 있다면 어떤 랜덤값이 나오는지 알 수 있다.

 

그럼 과연 진짜 랜덤값은 무엇일까?

 

<random> 라이브러리
  • 더 좋은 시드값을 위해 random_device 제공
  • std::random_device는 균일한 분포(uniformly-distributed)를 가진 비결정적(non-deterministic)인 int 숫자를 생성하는 클래스
  • 다양한 난수 생성 엔진과 분포(distribution)를 제공

 

 

std::random_device는 균일한 분포(uniformly-distributed)를 가진 비결정적(non-deterministic)인 int 숫자를 생성하는 클래스이다.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <random>

using namespace std;
int main()
{
    time_t now = time(0);
    tm localTime;

    localtime_s(&localTime, &now);

    cout << "현재 시간: ";
    cout << localTime.tm_hour << ":"
        << localTime.tm_min << ":"
        << localTime.tm_sec << endl;

    random_device rd;

    cout << "Min Value : " << rd.min() << ", " << "Max Value : " << rd.max() << endl;

    for (int i = 0; i < 3; i++) {
        cout << rd() << endl;
    }

    return 0;
}

 

 

이렇게 하면 random_device가 제공하는 최대 최소값으로 랜덤값에 접근할 수 있다.

하지만 내가 범위를 지정하고 싶다면?

 

바로 분포를 이용하면 된다

#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dist(0, 99); // 0~99 범위임의로 지정했따

    for (int i = 0; i < 5; ++i) {
        std::cout << dist(gen) << " ";
    }
    return 0;
}

이렇게 겉으로 보기엔 좀 어려워보이지만 클래스 선언만 하고 분포값만 지정해주면 간단하게 진짜 랜덤한 값을 뽑아낼 수 있는 것이다.

 

정리해보자면 radnom_device로 랜덤한 시드를 받아온 다음, 이 시드를 기반으로 mt19937에서 알고리즘이 동작해서 랜덤값이 나오게 되는 것이다.

 

마무리말

여기까지 랜덤값을 어떻게 가져오면 균등한 확률로 뽑아낼 수 있을지에 대해 알아보았다.

공부하다보니 아직 배울 내용이 너무 많다는 걸 느꼈고 배웠던 내용도 시간이 지나면 잊어버리게 되어 꾸준히 복습하는 것이 중요하다는 것을 느꼈다.

그리고 구현되어있는 내장함수의 구조는 어떤지, 무슨 타입의 값을 받고 리턴하는지 자세히 들여다볼 수 있는 기회가 되었다.

 

'C++' 카테고리의 다른 글

[ C++ ] Text_RPG 팀플 완료  (1) 2025.01.17
[Visual Studio] 한글 깨짐 현상  (0) 2025.01.15
[C++] Text RPG 몬스터 생성 로직 설계  (1) 2025.01.13
[C++] 컴파일러와 인터프리터?  (1) 2025.01.03
[C++] Template, STL  (0) 2025.01.02

간단하게 Text를 통해 RPG 게임을 만드는 팀 프로젝트를 진행 중이다.

프로그램을 시작하면 플레이어 생성을 하고 플레이어의 레벨에 맞는 몬스터를 생성한다.

그리고 플레이어 또는 몬스터의 체력이 0이 될 때까지 전투를 진행한 뒤 보상을 받고 상점 방문 여부를 받는다.

그 후부터는 보스를 처치하거나 플레이어의 체력이 0이 될 때까지 계속 반복하는 흐름이다.

 

나는 여기서 몬스터 구현을 담당했다.

로직을 꼼꼼하게 설계하면 코드 작성이 쉬워지기 때문에 설계에 많은 시간을 쏟았다.

아래는 기능의 흐름과 어떤 것들을 구현하면 좋을지 몬스터 생성 관련 로직들을 설계한 내용이다.

각 클래스가 가지고 있어야할 멤버 변수와 멤버 함수들을 정의했다.

팀 회의를 통해 공통된 특성은 Actor라는 상위 클래스로 정의해서 상속받아 사용하는 방식으로 채택했다.

 

설계한 내용은 아래와 같다.

- 몬스터 생성은 랜덤으로 진행
- 체력과 공격력은 플레이어의 레벨을 받아 플레이어의 수준에 맞게 랜덤값을 부여
- 몬스터 종류는 총 3개의 종류로 설정
- 추후 각 몬스터별 스킬이나 새로운 기능을 추가할 가능성이 있으므로 각각의 파일을 따로 만들어서 관리
- 몬스터 생성은 Factory 클래스를 만들어서 생성 관련 로직만 담당


이렇게 설계한 내용으로 코드를 작성해보았다.

#pragma once
#include "Actor.h"
#include <memory>

class ZombieFactory
{
public:
    ZombieFactory();
    std::unique_ptr<Actor> CreateZombie(const int playerLevel);
};

 

#include "ZombieFactory.h"
#include "NormalZombie.h"
#include "VariantZombie.h"
#include "HalfZombie.h"
#include "BossZombie.h"
#include "RandomUtil.h"
#include <iostream>

using namespace std;

ZombieFactory::ZombieFactory()
{
}

unique_ptr<Actor> ZombieFactory::CreateZombie(const int playerLevel)
{
    int randomValue = RandomUtil::GetRandomInt(1, 3);

    // Player 레벨이 10 이상일 때 보스 좀비 생성
    if (playerLevel >= 10)
        return make_unique<BossZombie>(playerLevel);

    // 랜덤으로 나온 값을 바탕으로 좀비 생성
    // TODO : main에서 랜덤 값 정의하고 좀비 타입만 넣어서 하는 완전히 분리된 좀비팩토리 만들면 좋을듯
    switch (randomValue)
    {
        case 1:
            return make_unique<NormalZombie>(playerLevel);
        case 2:
            return make_unique<VariantZombie>(playerLevel);
        case 3:
            return make_unique<HalfZombie>(playerLevel);
        default:
            return nullptr;
    }
}

 

ZombieFactory는 Actor를 기반으로 세 종류의 좀비 클래스들을 동적 할당해서 포인터로 반환한다.

CreateZombie()는 플레이어의 레벨을 받아서 레벨에 맞게 몬스터들을 생성해주는 역할을 하도록 코드를 짰다.

그리고 매개변수로 받은 플레이어 레벨값은 받아서 전달만 하고 따로 변경하거나 하지는 않아서 const를 붙여줬다.

만약 플레이어 레벨이 10 이상이라면 보스 몬스터를 출현하도록 했다.

 

랜덤값을 부여할 때 rand() 함수를 생각했었는데, 검색해보니 rand()는 비효율적이라고 한다.

rand()는 0 ~ 9까지의 시드?를 사용하는데 만약 사용자가 랜덤 범위를 0에서 3까지로 설정했다면

0, 1, 2, 3, 4, 5, 6, 7, 8, 9 의 공간이
0, 1, 2, 3, 0, 1, 2, 3, 0, 1 로 설정되면서 0과 1이 나올 확률이 더 높아지기 때문에 정밀한 랜덤값을 추출하는데 한계가 있다고 한다.

시드?라는 용어를 사용했고 왜 비효율적인지 자세히 이해는 못했다. 다시 한 번 공부를 해야할 내용이다.

 

그리고 아래는 각 좀비타입별로 분리해놓은 좀비 클래스이다. 이름만 다를 뿐 전체 로직은 같다.

#pragma once

#include "Actor.h"
#include <string>
#include <iostream>

class NormalZombie : public Actor
{
public:
    NormalZombie(const int playerLevel);

    std::string GetName() const;
    int GetMaxHP() const;
    int Attack() const;
    void OnHit(int inputAttackPower) const override;
    void PrintStatus() const;
};

공통 특성들을 정의해놓은 상위 클래스 Actor를 상속받아 진행했다.

Get이라는 키워드가 붙은 멤버함수들은 값을 따로 변경하거나 하는 부분 없이 그냥 값을 반환하는 형태의 로직이라 모두 const를 붙여주었다.

 

#include "NormalZombie.h"
#include "RandomUtil.h"

using namespace std;

NormalZombie::NormalZombie(const int playerLevel) : Actor("", 0, 0)
{
    Name = "NormalZombie";
    MaxHP = RandomUtil::GetRandomInt(playerLevel * 20, playerLevel * 30);;
    AttackPower = RandomUtil::GetRandomInt(playerLevel * 5, playerLevel * 10);
    HP = MaxHP;
}


string NormalZombie::GetName() const
{
    return Name;
}

int NormalZombie::GetMaxHP() const
{
    return MaxHP;
}

int NormalZombie::Attack() const
{
    return AttackPower;
}

void NormalZombie::OnHit(int inputAttackPower) const
{
    Actor::OnHit(inputAttackPower);

    cout << Name << "는 " << inputAttackPower << "의 데미지를 입었다..!" << endl;
    
    if (IsDead())
        cout << Name << "는 마지막 일격으로 사망했습니다 !!!" << endl;
}

void NormalZombie::PrintStatus() const
{
    cout << "Name : " << Name << endl;
    cout << "Attack power : " << AttackPower << endl;
    cout << "Current health point : " << HP << endl;
}

생성자는 플레이어의 레벨을 입력받아 랜덤한 값으로 체력과 공격력을 초기화한다.\

 

체력과 공격력을 부여하는 부분에서 랜덤값이 반복해서 사용되었기 때문에 랜덤값을 뽑아내는 부분을 따로 클래스를 만들어서 유틸리티 형태로 만들었다.
(다른 클래스에서 랜덤값을 사용할 일이 있을 것 같아 다른 팀원분들이 사용하기 편하라고 만들었다)

#include "RandomUtil.h"

using namespace std;

namespace RandomUtil
{
	default_random_engine generator(random_device{}());

	int GetRandomInt(int min, int max)
	{
		uniform_int_distribution<int> randomValue(min, max);
		return randomValue(generator);
	}
}

하지만 위에서 언급했듯이 아직 이 랜덤 엔진을 사용하는 방법이 미숙하고 왜 이 방법을 사용하는 것이 효율적인지 과정에 대해 이해를 못했기 때문에 다시 공부를 해야할 것 같다.

완벽하게 이해를 했다면 값이 들어갔을 때 시드를 공평하게 분배해서 평등한 확률이 나올 수 있도록 로직을 수정할 계획이다.

 

더 공부해야할 내용

- random 라이브러리의 동작 구조
- 포인터
- 업캐스팅, 다운캐스팅

 

마무리 말

글로 로직을 설계하는 것은 할 수 있는데 내가 짠 코드를 막상 설명하려고 하니까 말이 제대로 안나오고 뇌절하는 상황이 벌어졌다.

아직 개념이 제대로 정리되지 않았는데 말로 설명하려니 어렵게 느껴졌던 것 같다.(이럴까봐 gpt는 되도록 사용하지 않았는데...)

좀비 클래스를 구현하면서 사용했던 문법이나 개념을 다시 한번 확실하게 잡고 누군가에게 설명할 수준이 되도록 학습을 진행해야겠다.

 

 

'C++' 카테고리의 다른 글

[Visual Studio] 한글 깨짐 현상  (0) 2025.01.15
[C++] 진짜 랜덤값? 가짜 랜덤값?  (0) 2025.01.14
[C++] 컴파일러와 인터프리터?  (1) 2025.01.03
[C++] Template, STL  (0) 2025.01.02
[C++] 유니크 포인터  (0) 2024.12.31