미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구
20강. 미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구
이번 강의에서 해결할 문제
문법 예제를 각각 실행하는 것만으로는 어떤 타입을 어디에 배치해야 하는지 판단하기 어렵습니다. 아이템 목록, 조회 실패, 퀘스트 상태, 소유권을 한 프로그램에 합치되 모든 책임을 main과 전역 데이터에 몰아넣지 않아야 합니다.
학습 목표
Item,Inventory,Quest의 책임을 나눕니다.vector, 알고리즘,optional,enum class를 하나의 데이터 흐름에 적용합니다.unique_ptr가 필요한 소유 지점과 값 객체로 충분한 지점을 구분합니다.- 정상·부재·완료 전환 시나리오를 직접 검증합니다.
먼저 알아야 할 핵심 개념
Item과 Quest는 독립적으로 복사 가능한 값입니다. Inventory는 아이템 목록과 중복 합치기 규칙을 책임집니다. 조회 결과는 아이템이 없을 수 있어 optional<reference_wrapper<const Item>>로 표현합니다. 예제의 플레이어가 인벤토리를 동적으로 단독 소유하는 상황을 보여 주기 위해 unique_ptr를 사용하지만, 동적 수명이 필요 없다면 값 멤버가 더 단순합니다.
가장 작은 실행 가능한 예제
아래 코드는 포션 세 개를 모으면 퀘스트를 완료하는 전체 C++17 프로그램입니다.
#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
struct Item {
std::string name;
int count;
};
enum class QuestState {
Active,
Completed,
};
class Inventory {
public:
void add(std::string name, const int amount) {
const auto found = std::find_if(
items_.begin(), items_.end(),
[&name](const Item& item) { return item.name == name; });
if (found != items_.end()) {
found->count += amount;
return;
}
items_.push_back(Item{std::move(name), amount});
}
std::optional<std::reference_wrapper<const Item>>
find(const std::string_view name) const {
const auto found = std::find_if(
items_.begin(), items_.end(),
[name](const Item& item) {
return std::string_view{item.name} == name;
});
if (found == items_.end()) {
return std::nullopt;
}
return std::cref(*found);
}
int countOf(const std::string_view name) const {
const auto found = find(name);
return found ? found->get().count : 0;
}
void print() const {
for (const auto& item : items_) {
std::cout << item.name << ':' << item.count << '\n';
}
}
private:
std::vector<Item> items_;
};
struct Quest {
std::string title;
std::string requiredItem;
int requiredCount;
QuestState state{QuestState::Active};
};
void updateQuest(const Inventory& inventory, Quest& quest) {
if (quest.state == QuestState::Active
&& inventory.countOf(quest.requiredItem) >= quest.requiredCount) {
quest.state = QuestState::Completed;
}
}
std::string_view stateText(const QuestState state) {
return state == QuestState::Completed ? "completed" : "active";
}
int main() {
auto inventory = std::make_unique<Inventory>();
inventory->add("Potion", 1);
inventory->add("Key", 1);
inventory->add("Potion", 2);
Quest quest{"Collect potions", "Potion", 3};
updateQuest(*inventory, quest);
inventory->print();
std::cout << quest.title << ": " << stateText(quest.state) << '\n';
}
핵심 코드 줄별 설명
Item과Quest는 소유 자원이 없는 단순 값 타입입니다.Inventory::add는find_if로 같은 이름을 찾고 있으면 수량을 합칩니다.- 새 이름은
std::move(name)으로 목록의 문자열 소유권에 전달합니다. find는 아이템을 복사하지 않고 읽기 전용 참조를 optional 안에 반환합니다.countOf는 부재를 수량 0이라는 도메인 규칙으로 변환하는 한 지점입니다.updateQuest는 인벤토리를 읽고 퀘스트만 수정해 함수 계약이 드러납니다.- 퀘스트 상태는
enum class의 두 값만 허용합니다. main의unique_ptr가 인벤토리를 단독 소유하고 프로그램 종료 시 자동 정리합니다.
실행 결과와 값 변화
Potion:3
Key:1
Collect potions: completed
마지막 포션 추가량을 1로 바꾸면 총수량은 2이고 퀘스트는 active로 남습니다. Key 조회는 존재하지만 없는 Sword 조회는 nullopt가 됩니다.
단계별 실습
실습 목표
최소 기능부터 시작해 조회 실패와 퀘스트 상태 전환을 검증합니다.
시작 전 상태와 실행 방법
빈 C++17 콘솔 프로젝트에 위 코드를 inventory_quest.cpp로 저장합니다. 높은 경고 수준으로 먼저 그대로 빌드합니다.
1단계: 최소 코드 작성
Item, Inventory::add, print만 작성해 같은 아이템의 수량이 합쳐지는지 확인합니다.
2단계: 값 또는 조건을 바꿔 결과 비교
포션 총수량을 2와 3으로 바꾸어 퀘스트가 각각 active, completed가 되는 이유를 기록합니다.
3단계: 작은 기능 확장
수량이 0 이하인 추가 요청을 거부하고 성공 여부를 bool로 반환하세요. 호출자가 잘못된 입력을 구분해 출력하게 합니다.
4단계: 스스로 해결하기
다음 세 시나리오를 별도 함수 또는 assert로 검증하세요.
- 같은 아이템 두 번 추가 시 항목 수는 늘지 않고 수량만 합쳐진다.
- 없는 아이템 조회는 값이 없는 optional을 반환한다.
- 요구 수량 전에는 Active이고 충족한 뒤에만 Completed가 된다.
추가 과제로 variant<int, string>을 사용해 퀘스트 보상을 골드 또는 아이템으로 표현합니다.
막혔을 때
reference_wrapper 관련 오류는 <functional> 포함을 확인합니다. optional 결과의 원소는 found->get()으로 읽습니다. unique_ptr를 복사하려 하지 말고 함수가 소유권을 받을 필요가 없다면 Inventory& 또는 const Inventory&를 전달합니다.
흔한 실수와 해결 방법
- main에 모든 규칙 작성: 아이템 중복 처리와 조회를 Inventory 안으로 옮깁니다.
- find에서 Item 복사 반환: 읽기만 하는 조회가 큰 객체를 복사하지 않도록 참조 수명과 컨테이너 변경 규칙을 함께 관리합니다.
- 음수 수량 허용: 입력 경계에서 검증하고 잘못된 상태를 만들지 않습니다.
- unique_ptr를 목적 없이 사용: 실제 코드에서 플레이어와 수명이 같다면 값 멤버로 단순화합니다.
- 조회 참조를 vector 변경 뒤 보관: 원소 추가로 재할당될 수 있으므로 반환 참조는 짧게 사용합니다.
게임 개발 연결
이 구조는 게임의 인벤토리와 퀘스트 규칙을 UI 출력에서 분리하는 최소 예시입니다. Unreal로 옮길 때 std::vector와 표준 문자열을 기계적으로 교체하기보다 TArray, FName, UObject·USTRUCT의 수명과 리플렉션 요구를 다시 설계합니다. UI는 Inventory 규칙을 직접 수정하지 않고 공개 함수와 이벤트를 통해 반영합니다.
이해 점검 질문 3개
Item과Quest를 unique_ptr가 아니라 값으로 둔 이유는 무엇인가요?find가 optional과 읽기 전용 참조를 함께 사용하는 이유는 무엇인가요?- vector 변경 뒤 기존 원소 참조를 오래 보관하면 어떤 문제가 생길 수 있나요?
핵심 요약
모던 C++ 도구는 각각 따로 쓰는 문법 장식이 아닙니다. 상태는 enum class, 목록은 vector, 조회 부재는 optional, 지역 규칙은 알고리즘과 람다, 동적 단일 소유는 unique_ptr로 표현해 프로그램의 허용 상태와 책임을 읽을 수 있게 만듭니다.
다음 강의 연결
모던 C++ 심화에서 이 도구를 전제로 포인터 수명, RAII 구현, 공유 소유권, 복사·이동, iterator 무효화, 오류 설계와 동시성을 깊게 다룹니다.
미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구 미니 퀴즈
선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.
학습을 마쳤나요?
직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.