TIL
05.29 TIL (ch.2 팀프로젝트 day6)
2026. 5. 29. 20:00

1. 팀 프로젝트 진행

오늘 시간표 예정 :

오전: (코드카타(08:00~08:30), Atanny(08:30~09:00), 팀프로젝트에서 내가 작업한 코드 리뷰(09:00~11:00), 팀프로젝트 마무리 작업(11:00~13:00)

 

오후: 발표회(14:00~17:00), 팀프로젝트 리뷰(17:00~18:00), 코딩테스트 공부와 TIL 작성(19:00~21:00). 이후는 원래 하던대로 운동 1시간 후 휴식. 


1) 오전

팀프로젝트 소감

 

(학습한 개념)

RPG 게임 제작 과정에서 기본적으로 필요한 요소들과 전체 구조를 공부하고 직접 코드를 작성할 수 있었음. RPG 게임의 랜덤 로직이 적용되는 요소들이 무엇이 있는지 알아보고 이를 실제로 구현함. 특히 인벤토리와 Shop 연동, 전투 중 인벤토리 사용, 아이템 구조체, 몬스터 아이템 드랍 등 실제로 게임에서 사용될 여러 로직들을 직접 구현해보는 좋은 경험을 한 것 같음.

 

(협업 Tool)

실제로 여러 Tool을 사용해보고 팀원들과 상호작용 하는 좋은 경험을 했음.

1) visual studio : 혼자 공부할 때는 크게 신경쓰지 않았던 폴더 정리 방법을 고민해보게 되었음.

2) github : push, pull, commit 등 기본 기능을 사용해보고 팀 프로젝트에서 사용해보면서 기능 사용에 익숙해지게 되었음.

3) 데일리 스크럼 : 전날 뭐했는지, 마무리 시간에는 오늘 뭐할 건지 짧게 공유하고 기록함. 트러블 슈팅과 해결책을 공유하는 과정이 꼭 필요하다는 걸 느낌.

4) 칸반보드 : 할일 목록을 최대한 잘게 쪼개서 애매모호한 분업을 방지하고 진행 상황을 실시간으로 확인할 수 있는 점이 좋았음.

 

(총평)

처음에는 코드가 중복작성이 되거나 역할 분담이 모호해서 작업 우선순위가 밀리거나, 문제상황이 공유가 되지 않아 프로젝트 진행이 막히는 경우가 있었음. 무엇보다도 지금 단계에서는 문서를 통한 커뮤니케이션을 통해 문제 상황을 빠르게 공유하고, 진행 상황을 주기적으로 공유하려는 마인드를 심어놓는 것이 중요하다고 생각되었음. 그 외에는, 여러 디자인 패턴에 대한 공부를 추가적으로 하고 싶고, 게임 개발을 시작하기 전 전체 구조를 미리 그리고 구현할 기능들을 정하는 연습을 해보고 싶음.


팀프로젝트 코드 리뷰

 

내가 작업한 내용들이 어떻게 로직이 작성되어있는지 리뷰했다.

 

(1) GameManager.cpp

if ( mode == BattleMode::Auto )      //자동 전투
{
	while ( player->GetHealth( ) > 0 && !monsters.empty( ) )
	{
		// 플레이어의 공격
		player->Attack( );
		monsters[0]->TakeDamage(player->GetAttack( ));

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

			std::uniform_int_distribution<int> dist(10 , 21);		// 10 <= x < 21 
			int money = dist(engine);
			player->AddGold(money);
			std::cout << "골드 " << money << " 획득!" << std::endl;

			
			player->AddExperience(50);

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

		std::cout << std::endl;
		// 몬스터의 공격
		monsters[0]->Attack( );
		player->TakeDamage(monsters[0]->GetAttack( ));

		if ( player->GetHealth( ) <= 0 )			// 플레이어가 죽었을 시
		{
			std::cout << "전투 패배! " << std::endl;
			for ( Monster* m : monsters )
			{
				delete m;
			}
			monsters.clear( );
			return;//메인메뉴로
		}
	}
}

몬스터와의 전투에서 승리했을 때, FrameWork.h의 DropInfo 구조체를 이용해서, monster 각각에 저장된 아이템 리스트(아이템 각각에 float 값으로 드랍 가중치가 부여되어있음)를 받아와 dropTable을 생성하고, std::uniform_real_distribution<float> dropDistribution(0.0f, 1.0f); 선언했다.

이후에 난수를 이용해서 뽑아낸 확률과 drop.chance를 비교해서 조건에 부합하면 CreateItem(drop.type, 1); 을 하여 아이템을 생성하고 AddItem(dropItem);으로 인벤토리에 넣도록 했다.

 

(2) Character.h와 Character.cpp

//Item* 해제 위한 소멸자, 인벤토리 기능
~Character();
void AddItem(Item* item);
void ShowInventory( ) const;
void SellItem(int index);
void RemoveItem(int index);

int GetInventorySize( ) const;

Item* GetItem(int index) const;

void Equip(int inventoryIndex);	// 장비 장착

bool UseItemInBattle( ); // 전투 중에 아이템 사용(호출하면 됨)

인벤토리 기능 구현과 상점 기능(구매 및 판매)을 위해서 소멸자, 멤버 함수, Getter함수를 선언했음.

분업 과정에서 내가 전투 중 인벤토리를 열어 사용 아이템을 사용할 수 있는 기능의 구현을 맡아서, 실제 전투 루프 도중에 인벤토리를 여는 로직을 작성하는 팀원이 편하게 사용할 수 있도록 bool UseItemInBattle(); 을 따로 만들어두었음.

//아이템 메모리 누수 방지
Character::~Character() {
	for (Item* item : inventory) {
		delete item;
	}
	inventory.clear( );
}

//인벤토리 기능 구현 위해서 추가, AddItem 함수가 호출될 때 중복 검사
void Character::AddItem(Item* newItem) {
	for ( Item* item : inventory ) {
		if ( item->GetName() == newItem->GetName()) {
			item->AddCount(newItem->GetCount());

			std::cout << newItem->GetName( ) << "을(를) 획득했습니다." << std::endl;
			delete newItem;
			return;
		}
	}
	inventory.push_back(newItem);
	std::cout << newItem->GetName( ) << "을(를) 획득했습니다." << std::endl;
}

void Character::ShowInventory( ) const {
	
	std::cout << "\n=== [ 인벤토리 ] ===" << std::endl;
	std::cout << "현재 소지 골드 : " << gold << " G" << std::endl;
	if ( inventory.empty( ) ) {
		std::cout << "(비어 있음)" << std::endl;
		return;
	}
	for ( size_t i = 0; i < inventory.size( ); ++i ) {
		std::cout << i + 1 << ". " << inventory[i]->GetName( ) << " (" << inventory[i]->GetCount( ) << "개) "
			<< "(" << inventory[i]->GetSellPrice( ) << " G)" << std::endl;
	}
}

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

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

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

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;
		
			usableItems.push_back(static_cast<int>(i));
			displayIndex++;
		}
	}
	if ( usableItems.empty( ) ) {
		cout << "(비어 있음)" << endl;
		return false;
	}

	std::cout << "0. 뒤로 가기" << std::endl;
	std::cout << "사용할 아이템 번호를 입력하세요: ";
	int itemChoice;
	cin >> itemChoice;

	if ( itemChoice == 0 ) {
		return false;
	}

	if ( itemChoice > 0 && itemChoice < displayIndex ) {
		int targetInventoryIndex = usableItems[itemChoice - 1];
		Item* selectedItem = inventory[targetInventoryIndex];

		selectedItem->use(this);

		if ( selectedItem->GetCount( ) <= 0 ) {
			RemoveItem(targetInventoryIndex);
		}
		return true;
	}

	std::cout << "번호를 다시 입력하세요." << std::endl;
	return false;
}

아이템 메모리 누수 방지를 위해서 ~Character() 소멸자를 만들었다(인벤토리로 할당된 메모리 삭제). vector 형식의 inventory에서 Item 클래스 형태의 item을 받아와 멤버함수와 Getter함수를 이용해서 AddItem(), ShowInventory(), SellItem(), RemoveItem(), GetItem() 함수를 만들었다.

GetInventorySize()의 경우 Shop기능 구현시에 조건문안에 삽입해서 인벤토리 사이즈보다 큰 숫자를 입력했을 때 다시 입력하도록 하기 위한 함수다. 

UseItemInBattle()은 Item.h에 위치한 가상함수로 이를 사용하였다. vector<int> usableItems; 벡터를 따로 선언하여 인벤토리에서 조건문에 따라 '사용가능한' 아이템만 받아와서 벡터에 값을 담아 출력할 수 있도록 했다. 이 과정에서 강제 형변환에 주의하여 static_cast<int>(i)를 사용하였고, 이후 인덱스 번호와 아이템 선택 값이 일치하면 해당 아이템을 사용하게(RemoveItem())하였다.

 

[-> 개선안 : 아이템 클래스는 별도로 관리하고, Interface를 만들어서 아이템에 '사용가능함' 태그를 달아서 사용하는 것이 더 좋을 것 같다.]

 

(3) Item.h와 Item.cpp

#pragma once

#include "../../../FrameWork.h"

class Character;

class Item {
protected:
	std::string name;
	int count;
	int sellPrice;

public:
	//아이템 생성자
	Item(std::string itemName, int count, int price)
		: name(itemName), count(count), sellPrice(price) {
	}

	virtual ~Item() {}

	virtual void use(Character* character) = 0;
	
	int GetCount() const { return count; }
	
	void SetCount(int Count) {
		if ( Count < 0 ) {
			count = 0;
		}
		else {
			count = Count;
		}
	}

	void AddCount(int amount) { count += amount; }
	
	//아이템 이름 반환
	std::string GetName() const { return name; }

	//아이템 판매를 위한 가상 함수 추가
	virtual int GetSellPrice( ) const { return sellPrice; }

	//사용 가능한 아이템만 전투 중 인벤토리에 출력하기 위해 가상함수 추가
	virtual bool isUsableInBattle( ) const { return false; }

};

//체력 회복 초기 count 5
class HealthPotion : public Item {
private:
	int healthRestore;

public:
	HealthPotion(std::string name, int healthRestore, int count, int price)
		: Item(name, count, price), healthRestore(healthRestore) {
	}

	void use(Character* character) override;
	bool isUsableInBattle( ) const override { return true; }

};

//공격력 증가 (임시로 공격력 영구적으로 10 증가). 초기 count 5
class AttackBoost : public Item {
private:
	int attackIncrease;

public:
	AttackBoost(std::string name, int attackincrease, int count, int price)
		: Item(name, count, price), attackIncrease(attackincrease) {
	}

	void use(Character* character) override;
	bool isUsableInBattle( ) const override { return true; }
};

//강화석
class EnhancementStone : public Item {
public:
	EnhancementStone(std::string name , int count, int price)
		: Item(name , count, price) {
	}
	void use(Character* character) override;
};

Character 클래스를 전방선언하고, 아이템 생성자와 소멸자를 선언. 캐릭터가 특정 아이템을 사용할 수 있도록 use() 순수 가상함수를, 아이템 판매를 위해 판매 가격을 해당 아이템에서 받아올 수 있도록 GetSellPrice 가상함수를, 사용 가능한 아이템만 전투 중에 인벤토리에 출력할 수 있도록 isUsableInBattle() 가상함수를 추가했다. 이후 Item클래스에서 사용할 멤버함수, 인벤토리와 상점에서 사용할 Getter함수를 추가했다.

처음에는 아이템별로 class를 나누어 헤더파일로 분류하려 했으나 현재로서는 아이템 종류가 많지 않고 들어갈 기능이 적어서 Item.h 내부에 자식 클래스를 만들었다. 각각의 아이템은 void use(), bool isUsableInBattle()을 오버라이딩한다.

#include "Item.h"
#include "../Character.h"

void HealthPotion::use(Character* character) {

	if (count <= 0) {
		std::cout << "체력 포션이 없습니다." << std::endl;
		return;
	}

	count--;

	//변수를 선언 하고 getter함수 호출

	int currentHealth = character->GetHealth();
	int maxHealth = character->GetMaxHealth();

	//함수 수정(Entity.h의 AddHealth 함수 활용)
	if (currentHealth + healthRestore > maxHealth) {
		character->AddHealth(maxHealth - currentHealth);
	}
	else {
		character->AddHealth(healthRestore);
	}

	std::cout << "체력 회복 (현재 : " << character->GetHealth() << ")" << std::endl;
}

void AttackBoost::use(Character* character) {
	if (count <= 0) return;
	count--;

	//마찬가지로 Entity.h의 AddAttack 함수 활용
	character->AddAttack(attackIncrease);

	std::cout << "공격력 증가 (현재 : " << character->GetAttack() << ")" << std::endl;
}

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

Item.cpp에는 Item.h 내부의 자식 클래스에서 사용할 함수 오버라이딩을 구현했다. 같은 use() 함수를 다른 기능으로 구현하기 위함이다. 체력포션은 use(Character* character)를 받아와 체력포션의 숫자가 줄어들게 했고, 체력 회복을 하게 했다. 이는 AddHealth(maxHealth - currentHealth) 식으로 구현했고 체력 최대치를 넘지 않도록 했다. AttackBoost의 경우 현재로서는 아이템을 사용하면 공격력이 무조건 10이 오르도록 설정되어있다. 강화석의 경우 use()함수가 필요하지 않고, character.cpp에서 따로 강화석 사용로직이 구현되어있어 오버라이딩된 함수를 문자열만 출력하도록 만들었다. 즉, 인벤토리에서 강화석을 사용할 수는 없고 대장간에서만 강화석이 사용되고 강화 및 개수 차감되도록 로직이 구현되었다.

 

[->개선안 : 아이템 자식 클래스를 잘 구분해서 아이템 리스트를 만들기(대규모 프로젝트 연습), 공격력 포션의 로직을 수정해서 일시적으로(예를 들어, 전투 3회만 적용) 공격력이 상승하고 다시 원상태로 복구되는 로직으로 만들기]

 

(4) ItemManager.h와 ItemManager.cpp

#pragma once
#include "../../../FrameWork.h"

class Item;

class ItemManager {
private:
	static ItemManager* instance;
	ItemManager( ) {}

public:
	static ItemManager* getInstance( );

	Item* CreateItem(ItemType type , int count = 1);
};

ItemManager.h에서는 객체의 유일성을 보장하기 위해 싱글톤 디자인 패턴을 구현하려고 시도했다. Instance는 클래스를 바탕으로 메모리에 실제로 할당되어 실체화된 객체를 의미하며, 이렇게 실체화된 객체는 메모리의 데이터 영역에 단 하나만 생성되어 모든 인스턴스가 공유하도록  static(정적 멤버 변수)로 선언했다. 싱글톤 패턴을 구현하기 위한 핵심 멤버 변수 역할을 위해 작성했다.

이후 getInstance() 함수를 호출할 때 이 포인터에 힙영역의 ItemManager 인스턴스 주소를 최초 1회만 할당하게 하고 프로그램 어디서든 단일 인스턴스를 참조하여 상태를 공유할 수 있도록 했다. 

 

[->개선안 : 원시 포인터(*)를 사용한 싱글톤은 메모리 누수나 스레드 안전성 이슈가 발생하기 쉽다. 이를 방지하기 위해서 기존에 .h파일에 있던 static ItemManager* instance; 선언을 아예 삭제하고 getInstance() 함수 내부로 static 변수를 숨기는 방식을 채택하는 것이 좋다. 이후 * 포인터 대신 참조자(&)를 반환하여 더 안전하게 상태를 가져온다.]

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

ItemManager.cpp 에서는 getInstance() 함수와 Item* ItemManager::CreateItem(type, count) 를 구현했다. 무기와 갑옷이 드랍되거나, 상점에서 구매할 때 기본 능력치를 주기 위해서 Stats 배열을 선언하고 기본값을 대입했다. 이후 switch(type) case 문을 이용해서 아이템이 생성될 때 해당 정보를 가지고 나올 수 있도록 했다.

 

[->개선안 : 현재 CreateItem 함수 내부에 아이템의 이름이나 기본 스탯등이 하드코딩 되어있다. 지금 프로젝트에서는 아이템의 개수가 적어 관리하기가 어렵지 않지만, 나중에 기획이 변경되어 아이템의 수치를 바꿔야 한다거나 아이템을 새로 추가할 때마다 매번 코드를 수정하고 다시 빌드해야 하는 어려움이 생기게 될 것이다. 나중에 대규모 프로젝트에 참여할 때에는 아이템 속성 데이터를 외부 데이터 테이블(CSV 등)로 따로 빼는 방법을 공부해보자. 추가적으로 Item* 처럼 원시 포인터를 사용하고 있는데, 이를 C++의 std::unique_ptr을 사용해서 반환 타입을 std::unique_ptr<Item>으로 하게 되면 객체의 소유권을 명확히 나타내고 메모리가 자동으로 해제되도록 할 수 있다. #include <memory>를 추가하고, return std::make_unique<HealthPotion>("체력 포션", 50, count, 50); 형식으로 수정하는 것이 좋다.]

 

(5) Shop.h와 Shop.cpp

#pragma once

class Character;

class Shop
{
public:
	//아이템 정보 출력, 아이템 선택 구매
	void ShowItems( ) const;
	void BuyItem(Character* player , int choice);

	//상점을 호출 인터페이스
	void OpenShop(Character* player);

};

Shop.h에서는 아이템 정보 출력, 아이템 선택 구매, 상점을 호출하는 인터페이스를 구현하기 위해 멤버함수를 선언했다.

#include "Shop.h"
#include "../Character.h"
#include "Item.h"
#include "ItemManager.h"
#include "../../../FrameWork.h"



void Shop::ShowItems( ) const
{
	std::cout << "\n======== [ 상 점 ] ========" << std::endl;
	std::cout << "1. 체력 포션 (50G)" << std::endl;
	std::cout << "2. 공격력 포션 (50G)" << std::endl;
	std::cout << "3. 랜덤 박스 (10G)" << std::endl;
	std::cout << "4. 아이템 판매" << std::endl;
	std::cout << "0. 상점 나가기" << std::endl;
	std::cout << "=========================" << std::endl;
}

//1~3 아이템 구입
void Shop::BuyItem(Character* player , int choice) {
	if ( choice == 1 ) {
		if ( player->GetGold( ) >= 50 ) {
			player->UseGold(50);
			player->AddItem(ItemManager::getInstance()->CreateItem(ItemType::HealthPotion, 1));
			std::cout << "남은 골드: " << player->GetGold( ) << " G" << std::endl;
		}
		else {
			std::cout << "골드가 부족합니다." << std::endl;
		}
	}
	else if ( choice == 2 ) {
		if ( player->GetGold( ) >= 50 ) {
			player->UseGold(50);
			player->AddItem(ItemManager::getInstance()->CreateItem(ItemType::AttackBoost, 1));
			std::cout << "남은 골드: " << player->GetGold( ) << " G" << std::endl;
		}
		else {
			std::cout << "골드가 부족합니다." << std::endl;
		}
	}
	//랜덤박스. 체력 포션(10%), 공격력 포션(10%), 강화석(20%), 5G(20%), 꽝(40%)
	else if ( choice == 3 ) {
		if ( player->GetGold( ) >= 10 ) {
			player->UseGold(10);
			std::cout << "랜덤 박스 오픈!" << std::endl;
			std::random_device rd;
			std::mt19937 gen(rd( ));
			std::uniform_int_distribution<int> dis(1 , 100);
			int randNum = dis(gen);

			if ( randNum <= 10 ) {
				std::cout << "체력 포션 당첨!" << std::endl;
				player->AddItem(ItemManager::getInstance()->CreateItem(ItemType::HealthPotion, 1));
			}
			else if ( randNum <= 20 ) {
				std::cout << "공격력 포션 당첨!" << std::endl;
				player->AddItem(ItemManager::getInstance()->CreateItem(ItemType::AttackBoost, 1));
			}
			else if ( randNum <= 40 ) {
				std::cout << "강화석 당첨!" << std::endl;
				player->AddItem(ItemManager::getInstance()->CreateItem(ItemType::EnhancementStone, 1));
			}
			else if ( randNum <= 60 ) {
				std::cout << "5G 당첨!" << std::endl;
				player->AddGold(5);
			}
			else {
				std::cout << "꽝" << std::endl;
			}
		}
		else {
			std::cout << "골드가 없습니다. 돈을 벌어오세요." << std::endl;
		}
	}
	else if ( choice == 4 ) {
		int sellChoice = -1;
		while ( sellChoice != 0 ) {
			player->ShowInventory( );
			std::cout << "판매할 아이템 번호를 입력하세요(0. 뒤로가기) :" << std::endl;
			std::cin >> sellChoice;

			if ( sellChoice > 0 && sellChoice <= player->GetInventorySize( ) ) {
				int index = sellChoice - 1;
				Item* soldItem = player->GetItem(index);

				if ( soldItem != nullptr ) {
					int price = soldItem->GetSellPrice( );
					player->AddGold(price);
					soldItem->AddCount(-1);
					if ( soldItem->GetCount( ) <= 0 ) {
						player->RemoveItem(index);
					}
				}
			}
			else if ( sellChoice != 0 ) {
				std::cout << "잘못된 번호입니다." << std::endl;
			}
		}
	}

	else if ( choice != 0 ) {
		std::cout << "숫자를 다시 입력하세요." << std::endl;
	}
}

//OpenShop 함수 -> 전투루프 끝나고 상점 방문 시 호출.
void Shop::OpenShop(Character* player) {
	int choice = -1;
	while ( choice != 0 ) {
		ShowItems( );
		std::cout << "번호를 입력하세요: ";
		std::cin >> choice;

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

각각 함수의 기능에 맞게 코드를 작성했다. 랜덤 박스의 경우 std::random_device rd; // std::mt19937 gen(rd()); // std::uniform_int_distribution<int> dis(1, 100); 을 사용해서 int randNum = dis(gen); 난수를 입력받아 랜덤박스 로직을 작성했다.

std::random_device rd;로 시드를 생성하고(난수 생성기),

std::md19937 gen(rd()); 는 메르센 트위스터라는 난수 생성 알고리즘으로서 re()를 통해 받은 시드를 바탕으로 엔진을 가동하게 된다.

이후 std::uniform_~ 은 위에서 받은 난수들을 받아서 % 형태로 바꾸어 활용하게 된다.

 

[ --> 개선점 : 현재 코드에서는 박스를 열 때마다 rd, gen, dis를 매번 새로 생성해서 리소스 비용을 많이 잡아먹는다. 이 부분은 클래스의 멤버 변수와 static으로 빼서 한 번만 초기화하고 재사용하는 것이 성능상 더 좋을 것이다.]

 

또한 전투 루프ㅜ가 끝나고 상점을 방문했을 때 편하게 호출할 수 있도록 OpenShop() 함수를 만들고, 해당 기능을 ShowItems()와 BuyItem() 함수를 사용해서 구현했다. 이렇게 만든 함수는 GameManager.cpp에서 Shop shop; // shop.OpenShop(player); // break; 형식으로 간단하게 사용되었다.

 


2) 오후

발표회에서 다른 팀들이 어떻게 프로젝트를 진행했는지 주의깊게 듣고, 대략적인 내용과 느낀점, 내 생각을 정리해 보았다.

 

(1) UI, 로그의 출력

전반적으로 우리 팀 프로젝트의 로그 출력과 UI 가 많이 부실하다고 느꼈다. 실력적인 격차가 존재해서 한계가 분명히 있는 부분이라고 생각하지만, 이를 감안하더라도 우리 팀의 경우에는 어떻게 하면 플레이어가 로그를 쉽게 읽고, 확인하고, 현재 상태를 확인할 수 있는지 플레이어의 경험 측면에서 고민이 많이 부족했던 것 같다. UI도 게임 경험의 큰 부분을 차지하는 만큼 개선을 확실히하고 상호작용 시에 피드백을 확실하게 줘서 게임의 몰입감을 더 늘려보자.

특히 텍스트가 한번에 출력되는 것이 아니라 타이핑되는 것처럼 출력되는 것이 인상 깊었다. 이렇게 텍스트를 출력하니까 더 그럴듯해보이고 플레이 하는 입장에서 글 읽기가 더 쉬워지고, 더 몰입감이 느껴지는 연출이라고 생각되었다. C++에서는 이러한 타이핑 효과를 스레드와 시간 관련 표준 라이브러리를 사용하여 깔끔하게 구현할 수 있다고 한다.

 

추가적으로 HP, MP, EXP의 경우 그냥 문자로만 출력할 것이 아니라 Bar형식으로 시각화해서 보여주는 것이 경험 측면에서 훨씬 좋았다. 왜 이런 단순한 생각을 못했는지 아쉬울 따름이었다.

상점 기능의 경우에도 소비 아이템과 장비 아이템을 별도로 분리해서 출력하여 가독성을 높이는 방법도 있었다. 

 

 

(2) 게임 플레이

발표회 중 한 팀이 리듬게임 형태처럼 타이밍을 맞추지 못하면 공격이 Miss가 나도록 구현했는데, 단순 자동사냥 RPG의 형식에서 조금의 킥을 줘서 플레이 경험을 개선하는 시도를 한 것이 인상깊었다.

인벤토리, 상태창 뿐만 아니라 게임 화면에 실시간으로 지도를 만들어서 실시간으로 플레이어의 위치를 표시하고, 주변의 몬스터 위치를 표시하고, 맵의 장애물과 길을 표시하도록 구현한 팀도 있었다. 어떻게 했는지 궁금하기에 나중에 시간 내어 공부해보기로 했다.

 

(3) 세이브와 서버 설계

세이브 기능의 경우는 생각은 했으나 굳이 구현하지 않았던 부분이었지만, 공부를 할 겸 나중에 시간 내어 공부해보기로 했다. 온라인 서버를 구축해서 아레나 기능, 1대 1 기능, 온라인 아이템 거래 시스템 등을 구현한 팀도 있었는데, 서버 설계의 경우 아직 해당 내용을 공부하기에는 기본 지식이 너무 부족하고 모르는 용어가 많아 프로젝트와 학습 스케줄을 따라가면서 차근차근 배워나가기로 했다.

 

(4) 게임 기획

메인 스토리 개념을 넣어서 챕터를 구분해놓을 수도 있었고, 챕터 별로 몬스터가 변하게 할 수도 있었다.
전투 중 할 수 있는 행동도 다양하게 구현한 것을 확인할 수 있었다. 공격, 방어, 스킬, 인벤토리, 도주 시스템을 구현했다. 좀 더 플레이어에게 여러가지 선택권을 부여하는 것이 기획단계에서 이루어지는 것이 좋아보인다.
전투의 경우 메인 스토리와 지역 토벌(자유 전투) 를 구분해 놓기도 한 것을 볼 수 있었다. 내 생각을 덧붙이자면 서브 퀘스트 시스템을 구현해서 잠깐 메인 스토리에서 이탈해서 서브 퀘스트를 즐길 수 있게 해보는 것도 좋을 것 같다는 생각을 했다.
전반적으로 크툴루 테마의 RPG 게임으로 텍스트와 그림을 잘 입힌 프로젝트를 진행했던 팀이 있었다. 단순한 판타지 모험이 아니라 전략 생존형 RPG를 목표로, 텍스트 기반의 몰입. 즉 플레이어가 직접 공포를 체감하도록 설계한 것을 보았다.

굉장히 인상 깊고 마음에 들었는데, 이런 식으로 게임의 방향성을 제대로 잡고 가는게 중요한 것 같다. 내가 생각하기에도 게임은 결국 몰입감이 중요하므로, 어떤 장르든 어떤 매체든 상관없이 플레이어가 그 순간만큼은 그 세계에 빠져들어서 게임을 하게 만들어야 한다고 생각하기 때문이다. 방치형 분재게임이든, 미니게임이든 상관 없이 몰입감이 가장 중요한 요소라고 생각해서 지금까지 발표 중에서 가장 좋은 게임 개발 방향성을 잡은 사례라고 느꼈다. 아주 인상깊었고 학습을 이어나가면서도 많이 참고할 프로젝트라고 생각되었다.
또, 핵심 시스템으로 정신력 수치를 도입. 제 2의 체력바로 정신 공격 노출 시 정신력이 감소하며 상태 이상, 돌발 행동, 캐릭터의 사망까지 겪게 되는 것 까지 매우 인상깊었다. 전반적으로 크툴루 신화에 다키스트 던전을 짬뽕시켜놓은 느낌이었으나 세계관, 게임 콘셉트, 핵심 시스템을 서로 잘 엮어 하나의 게임으로 만들어 내어 보기 좋다고 생각했다.

게임을 만들기 시작하는 건 결국 처음에 구조를 잘 잡아야 하는 것 같다. 그래야 팀원들 모두가 창의력을 발휘해서 여러 기능도 쉽게 추가할 수 있고, 여러 스토리를 입힐 수도 있고, 아이템도 여러개의 방향으로 만들어 낼 수 있을 것 같다.

 

(5) 트러블 슈팅

전반적으로 Git 사용법, 작업 충돌, branch 삭제 등 협업에 관련된 문제점이 많이 발생했다. 다들 비슷한 처지인 것 같은데, 결국 중요한 것은 사전에 Git Rule을 잘 정립하고 다 함께 공유하고 일정 관리를 잘 하는 것이 답이라고 생각한다. 무엇보다도 많이 사용해보고 많이 경험해보면서 자연스럽게 주의할 점을 머릿속에 박아두는 것도 좋은 방법이라고 생각되었다.

 

(6) 우리 팀 프로젝트의 피드백

랜덤 박스(가챠 시스템)의 경우 천장 시스템 구현을 한번 해보고, Git 전략을 보강해서 Git Rule을 만들고 서로 합의하는 과정을 계속 경험하는 것이 중요한 것 같다. main - dev - 개별 branch와 같이 Git Flow를 잘 짜서 진행하자.

또한, 클래스 다이어그램과 시퀀스 다이어그램을 직접 만들어보고 사전에 합의해서 미리 만들어두어야 팀프로젝트 진행이 수월해진다는 것을 익혀두자.

 

(7) 총평

또 하나 인상깊었던 것이, 다 같은 전투 루프를 '노드'를 사용해서 갈래길을 만들어 플레이어가 갈래길에서 방향을 선택하고 앞으로 나아가는 것을 시각화해서 보여준 팀이 있었다. 코드로 구현하는 것도 사실 어려운 일이었겠으나 AI의 도움을 받으면 구현 자체는 그리 어렵지 않다고 생각한다. 이제 문제는 최적화를 얼마나 잘하느냐? 에러를 얼마나 잘 잡아내느냐? 의 싸움이라고 생각을 하기 때문이다.

 

그건 그렇고, 같은 과제를 받아도 결과물이 달라지는 이유가 무엇일까?

뭐가 다르길래 이렇게 인상깊은 결과물들이 나오게 된 걸까? 사실 기본적인 기능은 다른게 없는데 조금의 킥을 주고 보여주는 방식을 다르게 하고, 재밌을 것 같은 기능을 추가하고... 이런 여러 과정들이 더해졌기 때문에 달라진 것이라고 생각한다. 실제로 인상깊었던 프로젝트들을 보면 참여한 팀원들의 노력과 정성이 느껴질 정도로 짜임새와 완성도가 있었다.

결국 첫번째 팀프로젝트를 경험하고 나서 느낀 바와 연관지어서 생각해보자면, 조금이라도 더 좋은 게임이 나오기 위해서는 팀프로젝트를 시작할 때 이정도 질문은 던질 수 있어야 한다고 생각이 들었다.

 

팀원들의 의지가 있는가? + 팀이 제시한 목표가 확실한가? + 자유롭게 아이디어를 개진할 수 있는 분위기인가? + 의견을 자유롭게 적을 수 있는 창구가 있는가? + 소통이 원활하게 이루어지며 진행상황을 잘 정리되는가?

 

이 정도가 이번 프로젝트에서 느낀 점을 총정리 해본 결과물이다. 배운 점도 느낀 점도, 남몰래 불만도 조금 있었던 프로젝트였기에 더욱 값진 경험이었던 것 같다. 부족한 점을 많이 개선하고 프로젝트에 참여하는 팀원으로서 역할을 잘 해낼 수 있는 자신감도 생긴 것 같다.


*오늘의 코드카타*

0부터 9까지의 숫자 중 일부가 들어있는 정수 배열 numbers가 매개변수로 주어집니다. numbers에서 찾을 수 없는 0부터 9까지의 숫자를 모두 찾아 더한 수를 return 하도록 solution 함수르 완성해 주세요.

numbers의 모든 원소는 서로 다릅니다.

 

처음에는 0에서 9까지 더한 수에서 배열에 있는 숫자를 빼는 개념으로 접근했다.

#include <string>
#include <vector>

using namespace std;

int solution(vector<int> numbers) {
    int answer = -1;
    int sum_all = 9 * 10 / 2;

    for (int i = 0; i < numbers.size(); i++) {
        sum_all -= numbers[i];
    }
    answer = sum_all;
    return answer;
}

 

좀 더 고급스럽게 C++ 헤더파일을 사용해보자면 다음과 같이도 작성할 수 있다. accumulate를 이용하는 것이다.

전체 합인 45에서 입력받은 numbers 배열 내부 요소들의 합을 빼면 되기 때문에, #include <bits/stdc++.h> 헤더 파일을 사용해서 C++의 모든 표준 라이브러리를 한 번에 포함시키고(vector, numeric 헤더), accumulate(numbers.begin(), numbers.end(), 0)을 사용해서 vector의 처음부터 끝까지 모든 원소를 순회하며 더한다. 이때 세번째 0 인자는 덧셈을 시작할 초기값을 의미한다.

#include <bits/stdc++.h>
using namespace std;

int solution(vector<int> numbers) {
    return 45 - accumulate(numbers.begin(), numbers.end(), 0);
}

 

아니면 문제를 그대로 읽는 방식으로 이렇게 작성할 수 있다. 10칸짜리 빈 배열을 선언하고, 일치하는 숫자가 있다면 해당하는 인덱스 번호의 배열 값을 1로 만들어 준다(count). 그리고 인덱스 번호의 원소 값이 0이라면(일치하는 숫자가 없다면) answer에 해당하는 인덱스 번호를 더한다. 배열 원소의 값이 0~9이므로 인덱스 번호와 일치하므로 할 수 있는 방식이다.

#include <string>
#include <vector>

using namespace std;
int a[10];
int solution(vector<int> numbers) {
    int answer = 0;

    for(int i=0;i<numbers.size();i++){
        a[numbers[i]]=1;
    }
    for(int i=0;i<10;i++){
        if(a[i]==0) answer+=i;
    }
    return answer;
}

 


2. 그 외

[프로그래머스]

 

주사위의 개수

#include <string>
#include <vector>

using namespace std;

int solution(vector<int> box, int n) {
    int answer = 0;
    
    int x = box[0] / n;
    int y = box[1] / n;
    int z = box[2] / n;
    
    answer = x * y * z;
    
    return answer;
}

핵심은 가로, 세로, 높이를 각각 n으로 나눈 뒤에 곱해야 주사위의 개수가 구해진다는 것. 부피를 부피로 나누면 안된다. 정육면체는 쪼개질 수 없기 때문이다.


합성수 찾기

#include <string>
#include <vector>

using namespace std;

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

하지만, 좀더 생각해보면 굳이 i를 j로 나누는걸 계속 반복할 필요는 없다. 약수를 끝까지 다 셀 필요 없이 중간에 1과 i을 제외한 숫자를 하나라도 발견한다면 바로 합성수로 인정하고 다음 숫자로 넘어가면 되기 때문이다.

using namespace std;

int solution(int n) {
	int answer = 0;
    
    for (int i = 4; i <= n; i++) {
    	for (int j = 2; j < i; j++) {
        	if (i % j == 0) {
        		answer++;
            	break;
        	}
    	}
    }
    return answer;
}

이런식으로, 어차피 1,2,3은 합성수가 아니므로 i는 4부터 시작하고, j는 2부터 시작해서 i-1까지만 나누어본다. j가 커지면서 처음 조건식에 부합하는 숫자를 발견한 순간 break;로 안쪽 반복문을 즉시 탈출하고 다음 i로 넘어간다. 이런 방식으로 불필요한 계산을 줄이면서 성능을 조금이라도 더 높일 수 있다.


최댓값 만들기(1)

#include <string>
#include <vector>

using namespace std;

int solution(vector<int> numbers) {
    int answer = 0;
    int max_val = numbers[0]*numbers[1];
    
    for (int i = 0; i < numbers.size() - 1; i++) {
        for (int j = i + 1; j < numbers.size(); j++) {
            int current_val = numbers[i]*numbers[j];
            
            if (current_val > max_val) {
                max_val = current_val;
            }
        }
    }
    answer = max_val;
    
    return answer;
}

첫번째로 이중 반복문을 활용하여 곱했을 때 최대값이 갱신되면 최대값을 새로 덮어씌우는 방법을 사용했다.

 

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

using namespace std;

int solution(vector<int> numbers) {
    int answer = 0;
    int max_val = numbers[0]*numbers[1];
    
    for (int i = 0; i < numbers.size() - 1; i++) {
        for (int j = i + 1; j < numbers.size(); j++) {
            max_val = max(max_val, numbers[i] * numbers[j]);
        }
    }
   
    answer = max_val;
    return answer;
}

두번째로는 <algorithm> 헤더에 포함되어있는 max() 함수를 이용했다. max(첫번째 값, 두번째 값) 식으로 사용하면 둘 중 큰 값을 반환하므로, 최대값을 새로 덮어씌우는 과정에서 max() 함수를 사용할 수 있다.

 

*그런데, 이렇게 풀면 문제가 발생할 수 있다. 지금은 정수 배열에 음수 값이 없지만, 음수 값이 들어가게 된다면 계산식이 완전히 달라지게 된다. 또한, numbers 배열이 길면 길어질 수록 이중 반복문의 시간 복잡도는 O(N^2)이므로 성능이 기하급수적으로 낮아지게 된다.

 

: <algorithm>의 sort와 max를 사용하여 원소 중 두개를 곱해 만들 수 있는 최댓값 return하기

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

using namespace std;

int solution(vector<int> numbers) {
    int answer = 0;
    int n = numbers.size();
    
    sort(numbers.begin(), numbers.end());
    
    int max_positive = numbers[n-1] * numbers[n-2];
    
    int max_negative = numbers[0] * numbers[1];
    
    answer = max(max_positive, max_negative);
    return answer;
}

이렇게 sort() 함수를 사용해서 numbers 배열을 오름차순으로 정렬해놓는다. 음수가 있을 경우를 고려하면 왼쪽에는 가장 작은 값(음수), 오른쪽에는 가장 큰 값(양수)가 포함되어 있을 것이다. 이 때, 음수 * 음수 = 양수 값이라는 사실을 잊으면 안된다. 따라서, 오름차순 된 배열의 인덱스 0과 1, 인덱스 n-1과 n-2 값을 곱한 값을 max() 함수를 이용해서 비교한다.

 


팩토리얼

#include <string>
#include <vector>

using namespace std;

int solution(int n) {
    int answer = 0;
    
    int i = 1;
    
    while (n >= i) {
        n = n / i;
        i++;
    }
    answer = i - 1;
    
    return answer;
}

처음에는 나눗셈 방식을 생각했다. While문을 작성하고, 곱셈을 해야 하므로 int i = 1;로 초기화한다. n >= i 인 조건에서는 계속 n을 i로 나누는 과정을 반복한다. 그러다가 어느 순간 n보다 i가 크게되면 더이상 조건을 만족하지 않으므로 i를 반환한다. 이때, 문제에서 요구하는 값은 i! <= n 값이므로, i-1;을 반환해준다. 이 방식은 계산과정에서 값이 불필요하게 커지는 것을 나눗셈을 통해서 막을 수 있어 효율적이다.

 

굳이 반복문과 곱셈을 활용하겠다면 다음과 같이 풀 수도 있다.

using namespace std;

int solution(int n) {
    int factorial = 1;
    int i = 1;
    
    while (factorial <= n) {
        i++;
        factorial *= i;
    }
    return i - 1;
}

다만 이렇게 되면 만약 문제의 조건과 달리 n값이 훨씬 큰 값이 되면 오버플로우가 발생할 수도 있게 된다.

 

추가로 재귀함수 공부를 위해서, 팩토리얼은 재귀함수의 대표적인 사례이므로 코드로 구현해보았다.

// 팩토리얼을 구하는 재귀함수
int get_factorial(int num) {
    // 종료 조건: 1 이하의 숫자는 1을 반환하고 끝냄
    if (num <= 1) {
        return 1;
    }
    // 자기 자신(get_factorial)을 다시 호출하여 곱함
    return num * get_factorial(num - 1);
}

이런식으로 팩토리얼은 재귀함수로 나타낼 수 있다. fac(num) = fac(num - 1) * num 과 같은 식이다. 다만, 이러면 동일한 계산을 매번 1부터 다시 계산해야 하므로 실행 속도가 매우 느려진다. 

그렇다면 굳이 재귀함수를 쓰고 실행 속도도 빠르게 하고 싶으면 어떻게 할까? 이미 배운대로, 가지치기를 사용하거나 메모이제이션을 사용하면 된다. 이 경우에는 메모이제이션이 더 훌륭한 방법이다.

 

 (1) 가지치기 사용

#include <iostream>

using namespace std;

int findMaxI(int n, int i = 1, int current_fact = 1) {

    if (current_fact > n) {
        return i - 1; 
    } 
    return findMaxI(n, i + 1, current_fact * (i + 1));
}

int solution(int n) {
    return findMaxI(n); 
}

현재 계산된 팩토리얼의 값이 n을 초과했다면 더이상 계산할 필요 없이 즉시 종료하고, 초과하기 직전의 값이 정답이므로 i-1을 반환한다.

초과하지 않았다면 다음 단계(i+1)로 가서 탐색을 이어가게 되는데, 이때 다음 팩토리얼 값을 미리 계산해서 넘겨주는 것이다. 사실 반복문 while(factorial <= n)을 썼던 논리 자체가 가지치기라고 보면 된다.

 

 (2) 메모이제이션

#include <vector>

using namespace std;

vector<int> memo(15, 0); 

int getFactorial(int num) {
    if (num <= 1) return 1;

    if (memo[num] != 0) return memo[num];

    memo[num] = num * getFactorial(num - 1);
    return memo[num];
}

int solution(int n) {
    int i = 1;
    while (getFactorial(i) <= n) {
        i++;
    }
    return i - 1;
}

어렵게 생각할 필요 없이, 메모장(vector)을 만들어서 미리 값을 넣어두고 필요할 때 꺼내 쓰는 방식이다. 결과를 저장할 메모장 배열을 전역 변수로 선언하여 기록을 유지한다. 이후, num을 따라 가면서 num을 계산한 값을 memo에 넣어놓는다. 메모장에 이미 계산된 값이 있다면( if (memo[num] != 0 ), 계산하지 않고 해당 값을 꺼내쓰고, 처음보는 숫자라면 계산하고 메모장에 적어둔다. 이때 처음보는 숫자여도 이미 계산된 num-1 값이 메모에 있기 때문에 num만 곱하면 쉽게 계산할 수 있다.

 

 (3) 꼬리 재귀(Tail Recursion)

 

일반 재귀는 메모리(스택)낭비로 이어지게 할 수 있다. 돌아와서 해야 할 일이 있으므로 현재 상태를 메모리에 계속 살려두어야 하기 때문이다.

int normal_factorial(int n) {
    if (n <= 1) return 1;
    
    // factorial(n-1)을 먼저 다녀온 뒤에, 돌아와서 n을 곱해야 함.
    // '돌아와서 해야 할 일'이 있으므로 현재 상태를 메모리에 계속 살려둠.
    return n * normal_factorial(n - 1); 
}

 

꼬리 재귀는 이번에 해야 할 계산을 미리 다 해서 다음 차례에 아예 넘겨버리고 메모리에서 삭제하는 방식이다. 애초에 C++ 컴파일러에서 알아서 이를 파악하고 메모리를 단 한 칸만 사용하도록 최적화하기 때문에 가능하다.

int tail_factorial(int n, int acc = 1) {
    if (n <= 1) return acc;
    
    // (n * acc)를 미리 계산해서 다음 함수로 통째로 넘김.
    // 자기 자신을 부르는 것 외에 '돌아와서 해야 할 연산'이 없음.
    // 컴파일러: 기존 메모리 덮어씌움.
    return tail_factorial(n - 1, n * acc); 
}

 

[atanny]

 

1. Unreal 네트워크 구성 요소와 역할 이해

 

네트워크에서 클라이언트(Client)의 역할 

클라이언트의 가장 기본적인 역할은 플레이어의 조작을 받아들이는 것이다. 이 입력을 RPC(Remote Procedure Call, 원격 프로시저 호출)를 통해서 서버에 전달하며 요청한다. 클라이언트는 서비스나 자원을 요청하는 주체이다.

 

서버(Server)의 개념 

서버는 클라이언트의 요청을 기다리는 역할 뿐만 아니라, 요청을 처리하고 응답을 생성하며, 능동적으로 클라이언트에게 데이터를 푸시하기도 한다.

 

프로토콜(Protocol)의 정의 

프로토콜은 컴퓨터 간 데이터 통신을 위한 규칙과 약속을 의미한다. 데이터 형식, 전송 순서, 오류 처리 방법 등을 정의한다.

 

클라이언트와 서버의 역할 관계

클라이언트-서버 모델에서 클라이언트가 먼저 서버에 접속을 요청하고, 서버는 요청을 받아 응답하여 연결을 수락하거나 거부한다.

 

프로토콜이 정의하는 내용 

프로토콜은 통신 규칙을 정의하는 것으로서, 데이터 형식, 전송 순서, 오류 처리 등을 정의한다.

 

2.TCP와 UDP의 통신 방식 선택

 

TCP(Transmission Control Protocol)의 핵심적인 특징 :  

TCP는 연결 지향적인 프로토콜로, 3-way handshake를 통해 연결을 설정하고 데이터 전송의 신뢰성을 보장한다. 데이터 손실 시 재전송하며 순서를 보장한다.

 

FPS 게임에서의 플레이어 위치 동기화를 구현할 때 적합한 프로토콜 :  

FPS 게임의 위치 동기화는 실시간성이 가장 중요하므로, 낮은 지연시간을 제공하는 UDP를 사용하는 것이 적합하다. 일부 패킷이 손실되어도 다음 업데이트로 보정이 가능하다. TCP를 사용하면 패킷 손실 시 재전송으로 인한 지연이 발생하여 게임 플레이가 끊기는 현상이 생길 수 있다.

 

TCP와 UDP의 차이점 :  

TCP는 흐름 제어와 혼잡 제어를 제공하지만 UDP는 제공하지 않는다.

UDP는 TCP보다 헤더 크기가 작아 오버헤드가 적다.

UDP는 비연결형 프로토콜로 연결 과정이 없다.

UDP는 브로드캐스팅과 멀티캐스팅을 지원하며, TCP는 1:1 연결 기반이므로 브로드캐스팅을 지원하지 않는다.

 



**오늘의 총평**

요 근래 언리얼 엔진을 어떻게 잘 쓸까 C++ 코드를 어떻게 잘 작성할까 이런 공부만 해왔는데, 다른 팀들이 프로젝트 발표를 하는 걸 보고 어떻게 플레이어가 게임을 재밌게 즐길까, 어떻게 해야 재밌게 게임을 만들까, 이런 고민을 해본 좋은 시간을 가진 것 같다. 한번 쉬어간 느낌이지만 항상 본질을 잡아 놓아야 핵심적인 부분에 대한 공부를 효율적이고 빠르게 진행할 수 있을 것 같다는 생각을 했다. 추가적으로 TIL 정리와 그동안 공부했던 내용을 주말간에 정리하고 카테고리별로 묶어서 나중에 찾아보기 편하게 정리하기로 계획을 세웠다.