TIL
05.27 TIL (ch.2 팀프로젝트 day4)
2026. 5. 27. 20:52

1. 팀 프로젝트 진행


1) 오전 스크럼

최대한 역할 분담 세부적으로 (칸반 보드 활용: 시작 전 -> 진행 중 -> 확인 대기 -> 완료 - 디버그 / 보완)

몬스터 아이템 테이블 만들기

획득한 아이템 인벤토리에 저장하기

아이템 매니저에 아이템 정보 만들기

전투 종료 후 아이템 획득하기 (시작 전, 진행 중, 완료, 피드백 구분)

클래스 다이어그램 (게임 완성본 피드백용, 발표 용, TIL용) 추가

강화할 때 강화석 차감하기(조건식까지)

강화 후 스탯 적용하기

 

2) 오전 작업 내용:

  1. EnhanceStone -> EnhancementStone으로 명칭 통일
  2. 상점 방문 -> OpenShop() 호출 추가

또한, 기존에 작성했던 Shop.cpp의 OpenShop() 함수에서 오류를 발견해서 디버그했음.

//기존에 if 조건문이 while문 바깥에 위치해서 조건식이 사용되지 않는 문제점 발견하여 수정

void Shop::OpenShop(Character* player) {
	int choice = -1;
	while ( choice != 0 ) {
		ShowItems( );
		std::cout << "번호를 입력하세요: ";
		std::cin >> choice;

		if ( choice != 0 ) {
			BuyItem(player , choice);
		}
	}
}

 

Item 클래스, ItemManager클래스, Equipment 클래스가 꼬여있어서 재작업을 진행.

 

1. 우선 Item.cpp에 작성되어 있던 BaseArmor, BaseSword를 전부 삭제했음. 이후 Weapon.h, Armor.h를 수정함.

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <random>
#include <math.h>
#include <map>
#include <algorithm>
#include <cstdlib>
#include <ctime>


struct Stats
{
	int health;
	int atk;
	int lv;
};

FrameWork.h 에 Stats 구조체가 작성되어 있음. 따라서, 이 Stats 구조체를 받아와서 Weapon과 Armor 아이템을 만들 수 있게 함

//ItemManager.cpp 수정사항

#include "ItemManager.h"
#include "Item.h"
#include "Equipment/Weapon.h"
#include "EquipMent/Armor.h"

ItemManager* ItemManager::instance = nullptr;

ItemManager* ItemManager::getInstance( ) {
	if ( instance == nullptr ) {
		instance = new ItemManager( );
	}
	return instance;
}

Item* ItemManager::CreateItem(ItemType type , int count) {

	//Stats {health, atk, lv}
	Stats baseWeaponStats = { 0, 20, 1 };
	Stats baseArmorStats = { 50, 0, 1 };

	switch ( type ) {

	case ItemType::HealthPotion:
		return new HealthPotion("체력 포션" , 50 , count, 50);

	case ItemType::AttackBoost:
		return new AttackBoost("공격력 포션" , 10 , count, 50);

	case ItemType::EnhancementStone:
		return new EnhancementStone("강화석" , count, 20);

	case ItemType::Weapon:
		return new Weapon("검" , baseWeaponStats, 100, count);

	case ItemType::Armor:
		return new Armor("갑옷" , baseArmorStats, 100, count);

	default:
		return nullptr;
	}
}

 

그리고 기존에 Item.cpp에서 작성했던 use() 함수를 수정해 주었다. 이 함수는 인벤토리에서 기능하는 함수이므로 강화석을 인벤토리에서 사용했을 때 차감되지 않도록 바꾸었다.

//Item.cpp

void EnhancementStone::use(Character* character) {
	std::cout << "강화석은 대장간에서 사용 가능합니다." << std::endl;
}

 

 

또한, GameManager.cpp에서 기존에 작성되어 있던 함수를 수정했다. 

GameManager::IsEnchantSuccess(..) 형식으로 사용되고 있었는데, IsEnchantSuccess() 함수는 클래스 내부에 선언된 멤버 함수이므로 GameManager::를 삭제했다.

//GameManager.cpp 내부 수정

case 1:
{
				Weapon* currentWeapon = player->GetEquippedWeapon( );
				if ( currentWeapon == nullptr )
				{
					std::cout << "현재 장착 중인 무기가 없습니다! 먼저 무기를 장착하세요.\n";
					break;
				}
				
				int currentGold = player->GetGold( );
				Stats weaponStats = currentWeapon->GetStats( );
				int currentLv = weaponStats.lv;
				int cost = 100 * currentLv;

				if ( currentGold >= cost )
				{
					if ( player->UseEnhancementStone( ) == false )
					{
						std::cout << "인벤토리에 [강화석]이 없습니다!\n";
						break;
					}
					player->UseGold(cost);

					if ( IsEnchantSuccess(currentLv) == true )
					{
						std::cout << "강화 성공!\n";
						weaponStats.lv = currentLv + 1;
						weaponStats.atk += 10;

						currentWeapon->SetStats(weaponStats);
						break;
					}
					else
					{
						std::cout << "강화 실패!\n";
						break;
					}



				}
				else
				{
					std::cout << "골드가 부족합니다! (필요한 골드 : " << cost << "G)\n";
					break;
				}
}
case 2:
{
				Armor* currentArmor = player->GetEquippedArmor( );
				if ( currentArmor == nullptr )
				{
					std::cout << "현재 장착 중인 갑옷이 없습니다! 먼저 갑옷을 장착하세요.\n";
					break;
				}
				
				int currentGold = player->GetGold( );
				Stats armorStats = currentArmor->GetStats( );
				int currentLv = armorStats.lv;
				int cost = 100 * currentLv;

				if ( currentGold >= cost )
				{
					if ( player->UseEnhancementStone( ) == false )
					{
						std::cout << "인벤토리에 [강화석]이 없습니다!\n";
						break;
					}
					player->UseGold(cost);

					if ( IsEnchantSuccess(currentArmor->GetStats( ).lv) == true )
					{
						std::cout << "강화 성공!\n";
						armorStats.lv = currentLv + 1;
						armorStats.health += 10;

						currentArmor->SetStats(armorStats);
						break;
					}
					else
					{
						std::cout << "강화 실패!\n";
						break;
					}
				}
				else
				{
					std::cout << "골드가 부족합니다! (필요한 골드 : " << cost << "G)\n";
					break;
				}
				break;
}

 

size_t to int, possible loss of data 컴파일 에러가 발생해서 Character.cpp 내부의 함수를 수정했다.

기존에 int 형식으로 선언되어있던 함수를 static_cast<int>를 사용해서 알아서 형변환 시키도록 수정했다.

// Character.cpp

void Character::Equip(int inventoryIndex)
{
	if ( inventoryIndex < 0 || inventoryIndex >= static_cast<int>(inventory.size()) )return;	// 인벤토리에 없는 아이템을 가져오려 한다면

	Item* item = inventory[inventoryIndex];
	EquipMent* equip = dynamic_cast<EquipMent*>(item);
	if ( equip == nullptr )
	{
		std::cout << "장비 아이템이 아닙니다\n";
		return; // 장비아이템이 아니면

	}


int Character::GetInventorySize( ) const {
	return static_cast<int>(inventory.size( ));
}


void Character::SellItem(int index) {
	if ( index < 0 || index >= static_cast<int>(inventory.size() )) {
		std::cout << "잘못된 번호를 입력했습니다." << std::endl;
		return;
	}
	Item* item = inventory[index];
	int sellPrice = item->GetSellPrice( );

	AddGold(sellPrice);
	item->AddCount(-1);

}

void Character::RemoveItem(int index) {
	if ( index < 0 || index >= static_cast<int>(inventory.size( ) )) return;
	delete inventory[index];
	inventory.erase(inventory.begin( ) + index);
    
 
 Item* Character::GetItem(int index) const {
	if ( index < 0 || index >= static_cast<int>(inventory.size( ) )) return nullptr;
	return inventory[index];
}

 

추가로 UseEnhancementStone() 의 오류를 발견해서 수정했다.

if ( ( *it )->GetName( ) == "강화석" ) {
    delete *it;            // 객체 자체를 삭제해버림
    inventory.erase(it);   // 인벤토리 칸을 날려버림
    return true; 
}

작성되어 있었던 함수는 이러했는데, 이렇게 되면 강화석을 한번 사용하게 되면 전부다 사라져버리는 불상사가 발생했다.

따라서 기존의 GetCount()를 사용해서 Count가 하나씩 줄어들 수 있도록 수정했다.

bool Character::UseEnhancementStone( )
{
	for ( auto it = inventory.begin( ); it != inventory.end( ); ++it ) {
		
		if ( ( *it )->GetName( ) == "강화석" ) {
			
			if ( ( *it )->GetCount( ) <= 0 ) {
				delete* it;
				inventory.erase(it);
			}
			return true;
		}
	}
	return false; 

}

 

추가로 argument : conversion from size_t to _Ty possible loss of data 오류도 해결하기 위해 Character.cpp의

bool Character::UseItemInBattle() 함수도 수정했다.

bool Character::UseItemInBattle( ) {
	vector<int> usableItems;
	std::cout << "==== [ 사용 가능한 아이템 ] ====" << std::endl;

	int displayIndex = 1;
	for ( size_t i = 0; i < inventory.size( ); ++i ) {
		if ( inventory[i]->isUsableInBattle( ) ) {
			cout << displayIndex << ". " << inventory[i]->GetName( ) << " (" << inventory[i]->GetCount( ) << "개)" << std::endl;
		
        //push_back(i) 가 아니라 명시적 형변환(Casting)을 통해 자료형 컴파일 에러 발생 가능성을 없앴음.
			usableItems.push_back(static_cast<int>(i));
			displayIndex++;
		}
	}
	if ( usableItems.empty( ) ) {
		cout << "(비어 있음)" << endl;
		return false;
	}
    
    
    //....

 

 

3) 오후 작업 내용:

#include "Goblin.h"
#include <cstdlib>


Goblin::Goblin(int playerLevel)
    : Monster(
        "고블린",
        playerLevel * 20 + rand() % (playerLevel * 10 + 1),
        playerLevel * 5 + rand() % (playerLevel * 5 + 1)
    )
{
	dropTable.push_back({ ItemType::HealthPotion, 0.5f });
	dropTable.push_back({ ItemType::EnhancementStone, 0.5f });
}

기존에 Goblin.cpp 파일에 dropTable.push_back 을 활용해서 ItemType 구조체에 아이템과 함께 가중치를 넣어 놓은 것을 확인할 수 있다.

이를 활용해서 GameManager.cpp에서 전투 승리 시 아이템을 인벤토리에 집어넣는 것 까지 구현하려고 한다.

while ( player->GetHealth( ) > 0 && !monsters.empty( ) )
{
	int preHealth = monsters[0]->GetHealth( );		// 공격 받기 전 체력

	// 플레이어의 공격
	player->Attack( );
	monsters[0]->TakeDamage(player->GetAttack( ));
	//std::cout << monsters[0]->GetName( ) << "에게 " << player->GetAttack( ) << " 데미지!" << std::endl;
	//std::cout << monsters[0]->GetName( ) << "체력 " << preHealth << " -> " << monsters[0]->GetHealth() << std::endl;

	if ( monsters[0]->GetHealth( ) <= 0 )			// 몬스터가 죽었을 시
	{
		std::vector<DropInfo> dropTable = monsters[0]->GetDropTable( );
		std::uniform_real_distribution<float> dropDistribution(0.0f , 1.0f);

		for ( const auto& drop : dropTable ) {
			if ( dropDistribution(engine) <= drop.chance ) {
				Item* dropItem = ItemManager::getInstance( )->CreateItem(drop.type , 1);

				if ( dropItem != nullptr ) {
					player->AddItem(dropItem);
				}
			}
		}

		for ( Monster* m : monsters )
		{
			delete m;
		}
		monsters.clear( );
		return;
	}

구현 의도가 몬스터가 가지고 있는 아이템 배열(DropTable)에 애초에 드랍 확률(가중치)가 부여되어있고, 전투 승리시 engine에서 뽑아낸 난수를 가지고 드롭 여부를 결정한 후 가중치 이내의 조건을 충족하면 해당 아이템을 Create하고 dropItem으로 잡아준 후, dropItem을 AddItem() 함수로 인벤토리에 집어넣도록 구현했다.

 

디버깅 용으로 치트 아이디도 만들어보았다. 테스트를 위해서 괜히 노가다를 하지 않도록 하기 위함이다.

void GameManager::CreatePlayer()
{
	std::string name;
	std::cout << "플레이어 이름을 입력해 주세요: ";
	std::cin >> name;

	player = new Character(name);

	//나중에 삭제하시면 됩니다(치트).
	if ( name == "돈내놔" ) {
		std::cout << "100000G, 갑옷 한개, 무기 한개" << std::endl;

		player->AddGold(1000000);
		
		Item* addStones = ItemManager::getInstance( )->CreateItem(ItemType::EnhancementStone , 1000);
		if ( addStones != nullptr ) { player->AddItem(addStones); }

		Item* addArmor = ItemManager::getInstance( )->CreateItem(ItemType::Armor , 1);
		if ( addArmor != nullptr ) { player->AddItem(addArmor); }

		Item* addWeapon = ItemManager::getInstance( )->CreateItem(ItemType::Weapon , 1);
		if ( addWeapon != nullptr ) { player->AddItem(addWeapon); }
	}
	//여기까지가 치트

	Battle(BattleMode::Auto);
}

 

 

추가로 강화석 사용시 차감이 안되는 오류가 있어서 Setter함수를 Item.h에 SetCount를 만들고 사용시 1 차감, 0이하일 경우 사용 불가 기능을 추가했다.

//Item.h

void SetCount(int Count) {
	if ( Count < 0 ) {
		count = 0;
	}
	else {
		count = Count;
	}
}

 

오류를 수정하는 와중에 코드가 삭제된 것 같다. 다음에는 push 하기 전에 제대로 잘 살펴보자. (꼭)

//Character.cpp

bool Character::UseEnhancementStone( )
{
	for ( auto it = inventory.begin( ); it != inventory.end( ); ++it ) {
		
		if ( ( *it )->GetName( ) == "강화석" ) {
			
			( *it )->SetCount(( *it )->GetCount( ) - 1);	//<= 여기가 추가되었음

			if ( ( *it )->GetCount( ) <= 0 ) {
				delete* it;
				inventory.erase(it);
			}
			return true;
		}
	}
	return false; 

}

 

 


*오늘의 코드카타*

어떤 정수들이 있습니다. 이 정수들의 절댓값을 차례대로 담은 정수 배열 absolutes와 이 정수들의 부호를 차례대로 담은 불리언 배열 signs가 매개변수로 주어집니다. 실제 정수들의 합을 구하여 return 하도록 solution 함수를 완성해주세요.

 

--> 주어진 배열을 순회하면서 signs 배열에 따라 absolutes 배열의 값에 부호를 붙인 후, 모두 더해주는 방식으로 진행한다.

#include <string>
#include <vector>

using namespace std;

int solution(vector<int> absolutes, vector<bool> signs) {
    int answer = 0;
    
    for (int i = 0; i < absolutes.size(); i++) {
        if (signs[i]) {
            answer += absolutes[i];
        }
        else {
            answer -= absolutes[i];
        }
    }
    
    return answer;
}

 


2. 그 외

[프로그래머스]

특정 문자 제거하기

(1) 반복문을 사용해서 접근해보았다. 빈 문자열에 조건에 맞는 문자만 가져다가 붙이는 방식으로 구현한다.

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, string letter) {
    string answer = "";
    
    for (char c : my_string) {
        if (c != letter[0]) {
            answer += c;
        }
    }
    return answer;
}

(2) erase(remove()) 를 사용한다.

remove는 지워야 할 요소들을 컨테이너의 맨 뒤로 보내고, 쓰레기 값이 시작되는 위치의 반복자를 반환한다는 사실을 기억하자.

그래서 remove로 정리를 먼저 해두고, 그다음에 이제 쓰레기값들을 erase로 깔끔하게 지운 다음에 반환한다.

#include <string>
#include <algorithm>

using namespace std;

string solution(string my_string, string letter) {

    my_string.erase(remove(my_string.begin(), my_string.end(), letter[0]), my_string.end());
    
    return my_string;
}

 

각도기

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

int solution(int angle) {
    int answer = 0;
    
    if (0 < angle && angle < 90) {
        answer = 1;
    }
    else if (angle == 90) {
        answer = 2;
    }
    else if (90 < angle && angle < 180) {
        answer = 3;
    }
    else {
        answer = 4;
    }
    
    return answer;
}

 

양꼬치

#include <string>
#include <vector>

using namespace std;

int solution(int n, int k) {
    int answer = 0;
    
    answer = (n*12000)+(k*2000) - (n/10)*2000;
    
    return answer;
}

 

짝수의 합

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

int solution(int n) {     
    int answer = 0;
    for (int i = 0 ; i <= n; i+=2)
    {
            answer += i;
    }
    
    return answer;
}

 

배열 자르기

#include <string>
#include <vector>

using namespace std;

vector<int> solution(vector<int> numbers, int num1, int num2) {
    vector<int> answer;
    
    for (int i = num1; i <= num2; i++) {
        answer.push_back(numbers[i]);
    }
    
    return answer;
}

push_back 도 좋지만, 그냥 아예 numbers 배열에서 num1~num2 까지 잘라내는 방법도 있다. 단순하게 생각해보자.

#include <string>
#include <vector>

using namespace std;

vector<int> solution(vector<int> numbers, int num, int num2) {
	vector<int> answer(numbers.begin() + num1, numbers.begin() + num2 + 1);
    return answer;
}

 

외계행성의 나이

처음 아이디어는 10으로 나눠가면서 char(x + 'a') 꼴로 아스키코드를 사용해서 문자열로 변환하고 붙여나가는 방식을 생각했다. 여기서 주의할 점은 10으로 나눠가면서 나머지를 문자로 바꿔서 문자열을 만드는 과정에서 앞에 붙이기 위해 + answer를 뒤에 붙여야 한다는 것이다.

#include <string>
#include <vector>

using namespace std;

string solution(int age) {
    string answer = "";

    while (age > 0) {
        int temp = age % 10;
        
        answer = (char)(temp + 'a') + answer;
        
        age /= 10;
    }
        
        
    
    return answer;
}

또는 숫자를 문자 형태로 된 숫자로 바꾸고, 이를 순회하면서 문자에 해당하는 알파벳을 계산해서 answer에 추가하는 방법도 있다.

#include <string>
#include <vector>

using namespace std;

string solution(int age) {
    string answer = "";

    string age_str = to_string(age);
    
    for (char c : age_str) {
        answer += (c - '0') + 'a';
        
    }
        
    
    return answer;
}

 

진료순서 정하기

배열을 복사하고, 내림차순으로 정렬한다. 그리고 원본 배열의 몇 번째 위치와 일치하는지 찾고, 실제 순위를 정답 배열에 저장한다.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> solution(vector<int> emergency) {
    vector<int> answer;
    vector<int> temp_emergency = emergency;
    
    sort(temp_emergency.begin(), temp_emergency.end(), greater<int>());
    for (int i = 0; i < emergency.size(); i++) {
        for (int j = 0; j < temp_emergency.size(); j++) {
            if (emergency[i] == temp_emergency[j]) {
                answer.push_back(j+1);
                break;
            }
        }
    }
    
    return answer;
}

map을 활용해서 풀수도 있다. map은 키값을 기준으로 오름차순 정렬되는 특성을 이용하는 것이다.

#include <string>
#include <vector>
#include <map>
using namespace std;

vector<int> solution(vector<int> emergency) 
{
 	
    //1.
    auto amt = map<int, int>();
    for (auto& e : emergency) { amt[e]; }
    
    //2.
    auto order = amt.size();
    for (auto& e : amt) { e.second = order--; }
    
    //3.
    for (auto& e : emergency) { e = amt[e]; }
    return emergency;
}

1. 배열을 순회하면서 map의 Key값으로 환자의 응급도를 넣는다. 그러면 오름차순 정렬되므로 작은 값부터 map 내부에 정렬되게 된다.

2. map 내부에 오름차순으로 정렬되어 있으므로, 역순으로 순위(Rank)를 부여한다. map의 원소의 개수 = Rank 꼴등 순위 이므로 이를 이용해서 order--; 하며 응급도가 가장 큰 마지막 원소가 1순위를 부여받게 한다.

3. 다시 원본 배열을 순회하면서 참조자를 활용해서 e의 값을 변경하면 원본 배열의 값이 직접 바뀌도록 만든다. map에서 해당 응급도를 검색하고 저장해둔 순위( amt[e] )를 가져온 뒤, 그 값을 배열에 그대로 덮어쓴다.

4. 이렇게 하면 이중 for문보다 성능이 더 우수하다. 환자 수가 많아지면 이중 for문은 O(N^2) 가 되는데에 반해 map 자료구조의 특성을 이용한 방식을 사용하게 되면 $O(N \log N)$ 이 된다.

 

순서쌍의 개수

#include <string>
#include <vector>

using namespace std;

int solution(int n) {
    int answer = 0;
    
    for (int i = 1; i <= n; i++) {
        if (n % i == 0) {
            answer++;
        }
    }
    return answer;
}

이렇게 탐색하는 방법은 숫자가 커질 수록 시간 복잡도가 계속 커지게 된다.

그래서 예전에 배웠던 탐색 기법을 이용하는 방법을 사용해서 개선해보았다.

약수를 구하는 경우에는 n의 제곱근을 기준으로 작은 값을 탐색하고 answer에 2를 더해주면 된다. 주의할 점은 n이 완전 제곱수 (i*i =n) 일 경우 1만 더해주어야 한다.

#include <string>
#include <vector>

using namespace std;

int solution(int n) {
    int answer = 0;
    
    for (int i = 1; i * i <= n; i++) {
        if (n % i == 0) {
            if (i*i ==n) {
                answer += 1;
            }
            else {
                answer += 2;
            }
        }
    }
    return answer;
}

 

배열 만들기 5

문자열을 잘라낸다? substr이 생각난다.

#include <string>
#include <vector>

using namespace std;

vector<int> solution(vector<string> intStrs, int k, int s, int l) {
    vector<int> answer;
    
    for (const string& str : intStrs) {
        string subStr = str.substr(s, l);
        int num = stoi(subStr);
        
        if (num > k) {
            answer.push_back(num);
        }
    }
    return answer;
}

 물~론 num을 굳이 선언 후 초기화하지 않고, stoi 안에 집어넣는 식으로 할 수도 있다. 추가로, emplace.back을 쓰는게 더 좋다.

#include <string>
#include <vector>

using namespace std;

vector<int> solution(vector<string> intStrs, int k, int s, int l) {
    vector<int> answer;
    
    for (const string& str : intStrs) {
        int temp = stoi(str.substr(s,l));
        if (temp > k) {
            answer.push_back(num);
        }
    }
    return answer;
}

 

부분 문자열 이어 붙여 문자열 만들기

#include <string>
#include <vector>

using namespace std;

string solution(vector<string> my_strings, vector<vector<int>> parts) {
    string answer = "";
    
    for (int i = 0; i < my_strings.size(); i++) {
        string temp = my_strings[i].substr(parts[i][0], parts[i][1]-parts[i][0]+1);
        answer += temp;
    }
    
    return answer;
}

 

문자열의 뒤의 n글자

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, int n) {
    string answer = "";
    
    for (int i = my_string.length()-1; i >= my_string.length()- n; i--) {
        answer = my_string[i] + answer;
    }
    
    return answer;
}

이렇게 작성하니까, 예외 케이스 때문에 오류가 발생하면서 문제를 틀렸다. 어디가 문제였을까?

사실 my_string.length()는 일반 정수(int)를 반환하지 않는다. C++에서는 부호가 없는 양수형(size_t 또는 unsigned int)을 반환한다.

 

문자열 길이와 n 이 같은 경우, i >= my_string.length() - n 값이 0이 된다. 그런데 이때, 뒤에 i-- 때문에 i = -1 이 되고, 조건문 안으로 들어가게 된다.

C++은 int i = -1와 size_t = 0 을 비교할 때, int를 강제로 부호 없는 정수로 변환해버린다. 이때 약 42억이라는 양수가 되어버리면서 반복문이 멈추지 않고 계속 돌아가다가 Dump가 발생하는 것이다.

그러면 단순하게, .length()의 결과를 우리가 아는 일반 정수(int)로 바꿔서 계산하면 된다.

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, int n) {
    string answer = "";
    
    int len = my_string.length();
    
    for (int i = len - 1; i >= len - n; i--) {
        answer = my_string[i] + answer;
    }
    
    return answer;
}

 

그래도 substr를 사용하는 게 제일 좋다.

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, int n) {
    string answer = "";
    
    int startIndex = my_string.length() - n;
    
    answer = my_string.substr(startIndex, n);
    //substr(startIndex) 가능. 어차피 끝까지 다 잘라내니까.
    
    return answer;
}

 


[atanny]

 

1. PlayerController와 GameInstance의 역할 이해

 

PlayerController의 주요 역할 :

플레이어의 입력을 처리하고 Pawn에게 전달, 카메라 제어 및 시점 관리, 사람 플레이어의 제어

 

GameInstance :

게임 애플리케이션이 시작될 때 생성되어 게임이 종료될 때까지 유지되는 싱글톤 객체

 

PlayerController와 GameInstance의 관계 :

레벨 간 유지해야 할 데이터는 GameInstance에 저장, PlayerController는 이를 참조

 

플레이어가 레벨을 전환할 때 :

PlayerController와 Pawn은 소멸되지만 GameInstance는 유지됨.

 

GameInstace는 World레벨에서 관리 :

따라서, GetWorld->GetGameInstance()로 접근한다.

 

2. 충돌 이벤트 처리와 충돌 타입 구분

 

언리얼 엔진의 충돌 이벤트 처리 흐름:

충돌 감지 -> 콜리전 채널 확인 -> 충돌 응답 결정 -> 이벤트 발생

 

Overlap과 Block의 차이:

Block은 물리 계산과 충돌 해결(collision resolution)을 수행하므로 더 많은 연산이 필요함. 따라서 트리거나 감지 영역 처럼 단순

용도로 사용하는 경우 Overlap을 사용하는 것이 효율적임.

 

Block 충돌 응답이 발생했을 때의 특징:

두 오브젝트가 물리적으로 막히며 Hit 이벤트가 발생함. (OnComponentHit 또는 OnActorHit)

 

3. 인터페이스와 다형성 이해

 

언리얼 엔진에서 Interface의 정의:

구현부 없이 함수의 선언(시그니처)만을 포함하며, 이를 구현하는 클래스들이 반드시 해당 함수를 구현하도록 강제하는 계약이다.

 

인터페이스를 사용하는 주요 목적:

서로 다른 클래스들이 동일한 함수 시그니처를 공유하도록 강제, 다형성을 통해 다양한 타입의 객체를 일관된 방식으로 처리, 클래스 간의 결합도를 낮추어 코드 유연성과 유지 보수성을 높임, C++ 다중 상속의 문제를 피하면서 여러 인터페이스 구현 가능.

 

언리얼 엔진의 인터페이스 구현 방식:

UInterface(리플렉션 시스템용), IInterface(실제 구현 및 사용용) 두 개의 클래스로 구성된다.

그래서, 실제로 인터페이스를 구현할 때는 I 접두가가 붙은 클래스(IMyInterface)를 상속받는다. 실제 인터페이스 함수들은 IMyInterface에 선언되고 이를 상속받아 구현한다.

 

4. Data Table 구조와 게임 데이터 관리

 

언리얼 엔진에서 Data Table의 정의:

구조체(Struct)를 기반으로 행과 열 형태로 게임 데이터를 체계적으로 관리할 수 있는 에셋이다.

 

Data Table을 생성하기 위해 필요한 것:

구조체(Struct), Row Name(각 행을 식별하기 위함. 문자열 형태로 저장), 

 


 

[C++ 학습]

 

코드 작성할 때 헤더 파일 인클루드 꿀팁(Visual Studio 기준)

../../../ 하다보니까 복잡해지기도 하고 작성하는데 과부하가 오는 경우가 몇몇 있었다. 특히 프로젝트 규모가 커지면 커질수록 더 머리 아플 것이라고 예상되기에 visual studio 기준으로 include 경로를 직관적으로 관리하는 방법을 알아보았다.

 

1) 개인 설정

프로젝트 속성 -> Properties -> 구성(Configuration)을 '모든 구성' 으로 변경 -> 플랫폼(Platform)을 '모든 플랫폼'으로 변경 -> '구성 속성(Configuration Properties) 메뉴 트리 찾고, C/C++, 일반(General) 순서로 클릭 -> 추가 포함 디렉터리(Additional Include Directories)란을 클릭 -> <편집> 선택하고 헤더 폴더 경로 추가(visual Studio의 매크로 사용) -> 적용 및 확인.

 

2) 팀프로젝트 할때는?

매크로 ( $(ProjectDir) ) 를 활용한 방식은 팀 프로젝트에서 발생할 수 있는 에러를 예방하는 정석적인 방법임. .vcxproj 에 저장되어 깃허브에 올라가게 되는데, 이렇게 매크로를 설정하면 각자의 환경에 맞춰서 경로가 동적으로 작동하므로 에러가 발생하지 않음.

다만 팀원이 새로운 라이브러리를 세팅하거나, 프로젝트 속성을 건드렸거나, 폴더 구조를 바꿨다면 팀 메신저에 프로젝트 설정 변경했으니 머지할 때 주의해 달라고 미리 소통하는 것이 기본. 그리고 당연하게도 숨김 폴더, debug, Release, x64 폴더 등이 깃허브에 올라가지 않도록 반드시 .gitignore 파일에 등록해서 push가 되지 않도록 막아야 함.

 

3) 몬스터의 드랍 정보 구조체(DropInfo) 사용 방법

 

FrameWork.h 상단에

#include "ItemManager.h"
struct DropInfo {
    ItemType itemType;  // 드랍할 아이템 종류 (ItemType::HealthPotion 등)
    int dropChance;     // 드랍 확률 (1 ~ 100%)
    int minCount;       // 최소 드랍 개수
    int maxCount;       // 최대 드랍 개수
};

이렇게 어떤 아이템이, 몇 퍼센트 확률로, 몇 개 떨어질 지 정의하는 구조체를 만든다. 이렇게 구조체를 정의해 놓으면 dropTable을 만들고 구조체에서 정의해놓은 형식대로 정보를 저장할 수 있다.

 

이후 Monster.h 클래스에 드랍 테이블을 추가한다.

#include <vector>

class Monster : public Entity {
protected:
    // 이 몬스터가 떨어뜨릴 수 있는 아이템 목록
    std::vector<DropInfo> dropTable; 

public:
    Monster(std::string n, int h, int a) : Entity(n, h, a) {}

    // 드랍 테이블을 반환하는 함수
    const std::vector<DropInfo>& GetDropTable() const {
        return dropTable;
    }
};

 

dropTable을 반환하는 함수를 만들었으면, 이제 자식 몬스터 생성자에서 각 몬스터를 생성할 때 고유의 드랍 테이블을 세팅해준다.

// Slime.cpp (슬라이름은 포션과 강화석을 드랍)
Slime::Slime(int playerLevel) : Monster("슬라임", 50, 10) {
    // 50% 확률로 체력포션 1~2개
    dropTable.push_back({ ItemType::HealthPotion, 50, 1, 2 });
    
    // 20% 확률로 강화석 1개
    dropTable.push_back({ ItemType::EnhancementStone, 20, 1, 1 });
}



// Orc.cpp (오크는 무기와 방어구도 드랍)
Orc::Orc(int playerLevel) : Monster("오크", 150, 30) {
    // 100% 확률로 강화석 1~3개 확정 드랍
    dropTable.push_back({ ItemType::EnhancementStone, 100, 1, 3 });
    
    // 5% 확률로 무기 드랍
    dropTable.push_back({ ItemType::Weapon, 5, 1, 1 });
}

 

이제 다 만들어졌으므로, GameManager에서 전투 승리시 로직을 적용시킨다.

//GameManager.cpp

if ( monsters[0]->GetHealth( ) <= 0 ) // 몬스터가 죽었을 시
{
    std::cout << "\n전투 승리! " << std::endl;

    // 1. 골드 및 경험치 획득 (기존 코드 유지)
    std::uniform_int_distribution<int> dist(10 , 21);
    int money = dist(engine);
    player->AddGold(money);
    std::cout << "골드 " << money << " 획득!" << std::endl;
    player->AddExperience(50);

    // 2. >>> 몬스터 드랍 테이블 처리 시작 <<<
    const auto& dropTable = monsters[0]->GetDropTable();
    
    for (const DropInfo& drop : dropTable) {
        std::uniform_int_distribution<int> chanceDist(1, 100);
        int roll = chanceDist(engine); // 1~100 사이 주사위 굴림

        if (roll <= drop.dropChance) { // 당첨!
            // 개수 주사위 굴림
            std::uniform_int_distribution<int> countDist(drop.minCount, drop.maxCount);
            int dropCount = countDist(engine);

            // ItemManager를 통해 아이템 생성
            Item* dropItem = ItemManager::getInstance()->CreateItem(drop.itemType, dropCount);
            
            if (dropItem != nullptr) {
                // 이전에 구현하신 AddItem 호출 (중복 시 알아서 개수만 합쳐지고 메모리 해제됨)
                std::cout << monsters[0]->GetName() << "이(가) 아이템을 떨어뜨렸습니다!" << std::endl;
                player->AddItem(dropItem);
            }
        }
    }
    // >>> 드랍 테이블 처리 끝 <<<

    // 몬스터 메모리 해제
    for ( Monster* m : monsters ) {
        delete m;
    }
    monsters.clear( );
    break; // 또는 return
}

dropTable에서 아이템 구조체를 만들어놓고, 실제 전투가 종료되면 랜덤으로 아이템을 뽑아내는 형식으로 구현하는 것이다.

 

 

 

4) 시퀀스 다이어그램?

시퀀스 다이어그램이란 UML(통합 모델링 언어)의 한 종류로, 여러 객체들이 시간이 지남에 따라 어떻게 상호작용하는지를 보여주는 설계도이다. 

객체(Lifeline) : 다이어그램 상단의 네모 박스와 아래로 뻗은 점선. C++의 클래스 인스턴스를 의미한다.

메시지(Message) : 객체 간에 이어지는 실선 화살표. C++에서는 한 객체가 다른 객체의 멤버 함수(메서드)를 호출하는 행위이다.

제어 블록(Activation Box) : 생명선 위에 그려지는 얇은 직사각형. 호출된 함수가 실행되고 있는 시간(스코프)을 의미한다.

반환(Return Message) : 점선 화살표로 표시되며, 함수 실행이 끝난 후 호출자에게 return 값을 돌려주는 것을 의미한다.

 

프로그램의 규모가 커지고 객체 간의 참조가 복잡해질 때, 시퀀스 다이어그램을 그리면 메모리 상에서 객체들이 어떻게 소통하는지 한눈에 파악할 수 있다. 게임 개발에서 주로 기술 설계 단계와 복잡한 핵심 시스템을 구현하기 직전에 그리는 것이 가장 좋다. 메모리 누수나 데드락(Deadlock, 두 개 이상의 스레드나 프로세스가 서로가 가진 자원을 내놓기를 무한정 기다리다 프로그램이 완전히 멈춰버리는 현상)같은 치명적인 구조적 결함을 미리 방지하기 위함이다.

 

 

5) 디자인 패턴? : 생성, 구조, 행동. 대표 사례만 알아두고 나중에 주말에 코드를 작성해보며 정리할 생각.

-생성 : (ex) 싱글톤(Singleton), 팩토리(Factory) 메서드, 빌더(Builder)

-구조 : (ex) 어댑터(Adapter), 데코레이터(Decorator), 파샤드(Facade)

-행동 : (ex) 전략(Strategy), 옵저버(Observer), 상태(state)

 

 

6) 클래스와 인터페이스.

 

클래스는 카테고리, 인터페이스는 태그.

클래스는 "이것의 정체는 무엇인가?"를 정의하고, 인터페이스는 "이것은 무엇을 할 수 있는가?"를 정의한다.

C++에는 interface 키워드가 따로 없어서 내부 구현이 없는 순수 가상함수로만 이루어진 클래스를 인터페이스처럼 사용하게 된다. 보통 이름 앞에 대문자 I를 붙여 구분한다.

예를 들어, 사과는 먹을 수 있으니 IUsable, 검은 장착할 수 있으니 IEquipment 태그를 달아주는 식이다.

 

dynamic_cast?

인터페이스와 함께 사용되는 것이 dynamic_cast이다.

dynamic_cast는 "너 그 태그가 있니?" 라고 묻는 것과 같다. 인벤토리에 여러 아이템이 섞여있는 상황에서, 해당 아이템이 먹을 수 있는 아이템인지, 장착할 수 있는 아이템인지 알아내고 싶을 때 dynamic_cast를 사용하는 것이다.

dynamic_cast<IEquipment*>(item) ...이렇게 작성하면, "이 아이템에 IEquipment 태그가 붙어있냐?" 라고 묻는 것과 같다. 그러면 컴퓨터는 탐색 과정에서 검을 발견하면 있다면서 장착 기능을 반환하게 된다. 만약 아니라면 nullptr로 실패 처리를 할 수 있다.

 

Up-Casting?

업캐스팅은 dynamic_cast와 짝을 이루는 톱니바퀴이다. ItemComponent처럼 중간 관리자가 모든 아이템을 한 번에 관리할 수 있도록 해준다. "자식 클래스를 부모 클래스인 척 취급"하는 것으로, 상속 관계도에서 아래에서 위로 올라간다고 해서 Up-Casting이라고 부른다.

std::vector<Item*> inventory;

inventory.push_back(new Sword());  
inventory.push_back(new Apple()); 
inventory.push_back(new Hammer());

이런식으로, ItemComponent는 아이템이 어떤 아이템인지는 모르겠지만 일단 내 리스트에 들어오고, 전부 Item 명찰을 달아주는 역할을 한다. 

즉, Item List에 넣을 때는 업캐스팅을 통해서 Item이라는 부모 형태로 통합해서 저장하고,

실제로 사용되거나 장착될 때 dynamic_cast(<IEquipment*>를 사용해서 안전하게 자식 클래스나 인터페이스로 다시 가져온다.

 

왜 이렇게 하는 걸까?

바로 커플링(결합도)을 낮추기 위해서이다.

Player 클래스 안에 Sword, Hammer 관련 코드가 직접 들어있으면, 새로운 무기 Bow가 추가될 때마다 Player 코드를 수정해야 한다. 게다가 Sword도 Player가 착용할 수도 있고, NPC가 착용할 수도 있다. 이렇게 매번 달라지는 요소가 생길 때마다 찾아가서 수정해야 하고, 수정 사항이 다른 곳에 영향을 미치는 경우가 많이 발생하게 되어 코드가 복잡해지고 연쇄적으로 뜯어고쳐야 하는 불상사가 발생할 수도 있다.

이렇게 클래스, 인터페이스 등을 구현해놓으면 Player는 무기들이 뭔지 모르는 상태로 놔둘 수 있다. 그냥 ItemComponent(중간 관리자)에게 이거 장착하고 싶다고 명령을 내리는 것 뿐이다. 새로운 무기가 여러개 추가되든, 장착하는 대상이 바뀌든 Player 코드는 단 한줄도 수정할 필요가 없어지게 된다.

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

// 1. 기본 아이템 클래스 (카테고리)
class Item {
public:
    std::string name;
    Item(std::string n) : name(n) {}
    virtual ~Item() = default; // 다형성을 위해 가상 소멸자 필수
};

// 2. 인터페이스 (태그: 장착 가능한가?)
class IEquipment {
public:
    virtual void Equip() = 0;   // 순수 가상 함수 (자식이 무조건 구현해야 함)
    virtual void Unequip() = 0;
    virtual ~IEquipment() = default;
};

// 3. 실제 아이템 구현 (다중 상속 활용)
class Sword : public Item, public IEquipment { 
public:
    Sword() : Item("강철 검") {}
    void Equip() override { std::cout << name << " 장착!\n"; }
    void Unequip() override { std::cout << name << " 해제!\n"; }
};

class Apple : public Item { 
    // IEquipment를 상속받지 않음 (장착 불가)
public:
    Apple() : Item("빨간 사과") {}
};

// 4. 중간 관리자 (ItemComponent)
class ItemComponent {
private:
    std::vector<Item*> inventory; // 아이템 리스트 관리
public:
    void AddItem(Item* item) { inventory.push_back(item); }

    void TryEquip(Item* item) {
        //**** 이 아이템이 IEquipment 인터페이스를 상속받았는지 검사한다. ****
        IEquipment* equipableItem = dynamic_cast<IEquipment*>(item);
        
        if (equipableItem != nullptr) {
            equipableItem->Equip(); // 상속받았다면 장착 실행
        } else {
            std::cout << item->name << "은(는) 장착할 수 없는 아이템입니다\n";
        }
    }
};

// 5. 플레이어
class Player {
public:
    ItemComponent itemComp; // 플레이어는 무기를 직접 갖지 않고 컴포넌트만 가짐.
};

int main() {
    Player p;
    Sword* mySword = new Sword();
    Apple* myApple = new Apple();

    p.itemComp.AddItem(mySword);
    p.itemComp.AddItem(myApple);

    // 플레이어는 중간 관리자에게 명령만 내림.
    p.itemComp.TryEquip(mySword); // 강철 검 장착
    p.itemComp.TryEquip(myApple); // 빨간 사과는 장착할 수 없는 아이템입니다

    delete mySword;
    delete myApple;
    return 0;
}

 

7) 프로젝트 진행할 때 폴더 구조 관리하는 방법

 

 1. 인터페이스와 구체적인 구현을 명확히 분리할 것.

 2. 언리얼 엔진은 헤더와 소스를 반드시 분리해야 하므로 숙지할 것. (public/ 폴더 : 헤더 파일, private/ 폴더 : cpp 파일)

 3. 데이터(Assets)와 로직(Source)의 엄격하게 분리할 것.

 

(일반 사례)

MyGameProject/
├── Assets/             # 🎨 게임 내 모든 리소스 (유니티나 언리얼의 Content 폴더 역할)
│   ├── Audio/          # BGM, 효과음
│   ├── Models/         # 3D 모델, 애니메이션
│   ├── Textures/       # 이미지, UI 스프라이트
│   └── Prefabs/        # 조립된 게임 오브젝트 데이터 (엔진 사용 시)
│
├── Source/             # 💻 C++ 소스 코드 (가장 중요한 부분!)
│   ├── Core/           # 게임 루프, 메모리 관리, 유틸리티 로직
│   ├── Systems/        # 물리, 렌더링, 입력(Input) 등 기반 시스템
│   ├── Gameplay/       # 실제 게임의 룰과 로직
│   │   ├── Components/ # ItemComponent 같은 각종 컴포넌트들
│   │   ├── Interfaces/ # IEquipment 등 순수 가상 함수로 이루어진 태그들
│   │   ├── Items/      # Sword, Apple 등 구체적인 아이템 구현부
│   │   └── Characters/ # Player, NPC, Monster 등
│   └── UI/             # 화면에 정보를 띄우는 로직
│
├── ThirdParty/         # 📦 외부 라이브러리 (사운드 엔진, 물리 엔진 등)
├── Docs/               # 📝 기획서(GDD), 시퀀스 다이어그램, API 문서 등
├── Config/             # ⚙️ 게임 초기 설정 파일 (.ini, .json 등)
└── Build/              # 🔨 컴파일된 실행 파일(.exe)과 임시 빌드 파일들이 모이는 곳

 

(언리얼 엔진 사례)

 

언리얼 엔진은 직접 작성하고 관리하는 곳엔진이 자동으로 생성하는 곳으로 나뉘어서 관리한다.

 

1) 직접 작성하고 관리(버전 관리가 필요함)

Source/ : C++ 코드가 들어가는 곳. 내부에 Public 과 Private 폴더를 나누어 캡슐화.

Content/ : 블루프린트, 3D 모델, 텍스처, 사운드 등 모든 에셋(.uasset)이 들어감. 프로그래머와 아티스트가 함께 작업하는 공간.

Config/ : 입력 키 세팅, 물리 엔진 설정, 해상도 등 프로젝트의 전반적인 환경 설정 파일(.ini)들이 모여 있음.

Plugins/ : 프로젝트에 종속된 커스텀 플러그인이나 마켓플레이스에서 받은 외부 기능들이 들어감.

 

2) 엔진이 자동으로 생성하는 곳(버전 관리 필요 없음)

Binaries/ : C++ 코드가 컴파일된 최종 결과물(.dll 또는 .exe)이 담기는 곳.

Intermediate/ : 컴파일 과정의 임시 파일(obj 파일 등)들이  저장됨(문제가 생기면 여기를 지우고 다시 빌드하는 경우가 많음).

Saved/ : 에디터의 자동 저장 파일, 크래스 로그, 로컬 설정 등이 저장됨.

'TIL' 카테고리의 다른 글

05.29 TIL (ch.2 팀프로젝트 day6)  (0) 2026.05.29
05.28 TIL (ch.2 팀프로젝트 day5)  (0) 2026.05.28
05.26 TIL (ch.2 팀프로젝트 day3)  (0) 2026.05.26
05.22 TIL (ch.2 팀프로젝트 day2)  (0) 2026.05.22
05.21 TIL (ch.2 팀프로젝트 day1)  (0) 2026.05.21