본문으로 건너뛰기
모던 C++LESSON 20

미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구

난이도초급 → 중급 입문
예상 시간45분
선수지식이전 강의

20강. 미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구

이번 강의에서 해결할 문제​

문법 예제를 각각 실행하는 것만으로는 어떤 타입을 어디에 배치해야 하는지 판단하기 어렵습니다. 아이템 목록, 조회 실패, 퀘스트 상태, 소유권을 한 프로그램에 합치되 모든 책임을 main과 전역 데이터에 몰아넣지 않아야 합니다.

학습 목표​

  • Item, Inventory, Quest의 책임을 나눕니다.
  • vector, 알고리즘, optional, enum class를 하나의 데이터 흐름에 적용합니다.
  • unique_ptr가 필요한 소유 지점과 값 객체로 충분한 지점을 구분합니다.
  • 정상·부재·완료 전환 시나리오를 직접 검증합니다.

먼저 알아야 할 핵심 개념​

Item과 Quest는 독립적으로 복사 가능한 값입니다. Inventory는 아이템 목록과 중복 합치기 규칙을 책임집니다. 조회 결과는 아이템이 없을 수 있어 optional<reference_wrapper<const Item>>로 표현합니다. 예제의 플레이어가 인벤토리를 동적으로 단독 소유하는 상황을 보여 주기 위해 unique_ptr를 사용하지만, 동적 수명이 필요 없다면 값 멤버가 더 단순합니다.

가장 작은 실행 가능한 예제​

아래 코드는 포션 세 개를 모으면 퀘스트를 완료하는 전체 C++17 프로그램입니다.

inventory_quest.cpp
#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';
}

핵심 코드 줄별 설명​

  1. Item과 Quest는 소유 자원이 없는 단순 값 타입입니다.
  2. Inventory::add는 find_if로 같은 이름을 찾고 있으면 수량을 합칩니다.
  3. 새 이름은 std::move(name)으로 목록의 문자열 소유권에 전달합니다.
  4. find는 아이템을 복사하지 않고 읽기 전용 참조를 optional 안에 반환합니다.
  5. countOf는 부재를 수량 0이라는 도메인 규칙으로 변환하는 한 지점입니다.
  6. updateQuest는 인벤토리를 읽고 퀘스트만 수정해 함수 계약이 드러납니다.
  7. 퀘스트 상태는 enum class의 두 값만 허용합니다.
  8. 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개​

  1. Item과 Quest를 unique_ptr가 아니라 값으로 둔 이유는 무엇인가요?
  2. find가 optional과 읽기 전용 참조를 함께 사용하는 이유는 무엇인가요?
  3. vector 변경 뒤 기존 원소 참조를 오래 보관하면 어떤 문제가 생길 수 있나요?

핵심 요약​

모던 C++ 도구는 각각 따로 쓰는 문법 장식이 아닙니다. 상태는 enum class, 목록은 vector, 조회 부재는 optional, 지역 규칙은 알고리즘과 람다, 동적 단일 소유는 unique_ptr로 표현해 프로그램의 허용 상태와 책임을 읽을 수 있게 만듭니다.

다음 강의 연결​

모던 C++ 심화에서 이 도구를 전제로 포인터 수명, RAII 구현, 공유 소유권, 복사·이동, iterator 무효화, 오류 설계와 동시성을 깊게 다룹니다.

MINI QUIZ

미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구 미니 퀴즈

선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.

0 / 2
  1. 문제 1“미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구” 내용을 실제 작업에 적용한 설명으로 가장 알맞은 것은 무엇인가요?
  2. 문제 2‘main에 모든 규칙 작성’ 실수를 판단할 때 “미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구” 강의가 제시한 기준은 무엇인가요?
LESSON STATUS

학습을 마쳤나요?

직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.

20강. 미니 실습: 플레이어 인벤토리와 퀘스트 상태 관리 도구 미완료 상태