본문 바로가기
게임 프로그래밍 기본/게임 자료구조

[게임 프로그래밍] 퀘스트 시스템 만들기 - 구조/디자인패턴

by 정보모아모아 2022. 12. 26.

게임 퀘스트 시스템을 만드는 방법을 알아봅시다. 

퀘스트 시스템은 요즘 어떤 게임을 만들든 간에 대부분 들어가게 되는 콘텐츠입니다.

퍼즐게임 등의 간단한 게임부터 MMO RPG 같은 큰 규모의 게임까지 많은 게임들에서 업적 또는 퀘스트 콘텐츠를 유저가 오랫동안 게임을 플레이할 수 있게 만들어 주는 지속성 콘텐츠로 사용하고 있습니다. 또는 스토리를 유저들에게 알리기 위한 컨텐츠로 활용하는 경우도 많습니다. 이런 퀘스트 시스템을 제작하는데 계획 없이 제작하게 되면 퀘스트 콘텐츠가 가지는 다양한 상태들(퀘스트 수락 전, 퀘스트 진행 중 등과 같은 것) 때문에 구조가 복잡해지고 그만큼 많은 오류를 불러오게 됩니다. 그래서 구조나 디자인 패턴이 중요하게 되는 것이죠.

그러면 이런 퀘스트 시스템을 제작하기 위한 구조 또는 디자인패턴에는 어떠한 것들이 있을까요?

 

 

C++를 이용해 게임 프로그래밍에서 퀘스트 시스템을 구현할 때 사용할 수 있는 설계 패턴은 여러 가지가 있습니다. 

  1. Command 패턴: 이 패턴은 요청을 객체로 캡슐화하여, 인자를 전달하거나 요청을 지연하기 쉽게 해 줍니다. 이 패턴을 사용하면, 게임 엔진에 의해 실행될 수 있는 커맨드로 퀘스트를 표현할 수 있습니다.
  2. State 패턴: 이 패턴은 객체의 특정 상태에 관련된 동작을 캡슐화하고, 구현과 완전히 분리할 수 있도록 해줍니다. 이 패턴을 사용하면, 퀘스트가 있을 수 있는 여러 상태(예: 시작하지 않음, 진행 중, 완료)를 표현할 수 있고, 각 상태의 동작을 정의할 수 있습니다.
  3. Observer 패턴: 이 패턴은 객체 간 한 대 다 종속 관계를 정의할 수 있게 해줍니다. 이는 한 객체의 상태가 변경될 때, 이에 종속된 모든 객체들이 자동으로 알림을 받고 업데이트될 수 있게 해줍니다. 이 패턴을 사용하면, 게임에서 일어나는 특정 이벤트(예: 특정 NPC가 제거됨, 특정 장소가 방문됨)를 퀘스트 시스템에 알리고, 그에 따라 퀘스트의 상태를 업데이트할 수 있습니다

 

각각의 패턴에 대한 예를 한번 살펴보도록 하겠습니다. 

 


 

1. Command 패턴 예시(Example)

 

먼저 퀘스트에 대한 추상 기본 클래스를 정의합니다.

class Quest {
 public:
  virtual void execute() = 0;
};

 

다음으로 구현할 퀘스트 유형별로 구체적인 클래스를 정의합니다.

class KillQuest : public Quest {
 public:
  KillQuest(int targetNpcId) : targetNpcId_(targetNpcId) {}
  void execute() override {
    // 퀘스트 실행로직 기입
  }
 private:
  int targetNpcId_;
};

class CollectQuest : public Quest {
 public:
  CollectQuest(int itemId, int quantity) : itemId_(itemId), quantity_(quantity) {}
  void execute() override {
    // 퀘스트 실행로직 기입
  }
 private:
  int itemId_;
  int quantity_;
};

 

마지막으로 다음 퀘스트 클래스의 인스턴스를 만들고 컨테이너(예제에서는 std::vector)에 저장합니다.

std::vector<std::unique_ptr<Quest>> quests;
quests.push_back(std::make_unique<KillQuest>(5));
quests.push_back(std::make_unique<CollectQuest>(10, 3));

 

그런 다음 퀘스트 목록을 반복하여 각 퀘스트를 실행할 수 있습니다

for (const auto& quest : quests) {
  quest->execute();
}

 


 

2. State 패턴 예시

먼저 퀘스트 상태에 대한 추상 기본 클래스를 정의합니다.

class QuestState {
 public:
  virtual void enter() = 0;
  virtual void update() = 0;
  virtual void exit() = 0;
};

 

퀘스트가 있을 수 있는 각 상태에 대한 구체적인 클래스를 정의합니다.

class NotStartedState : public QuestState {
 public:
  void enter() override {
    // Perform any necessary setup when entering this state
  }
  void update() override {
    // Check if the quest should start
    // and transition to the InProgressState if it should
  }
  void exit() override {
    // Perform any necessary cleanup when exiting this state
  }
};

class InProgressState : public QuestState {
 public:
  void enter() override {
    // Perform any necessary setup when entering this state
  }
  void update() override {
    // Check if the quest should be completed
    // and transition to the CompletedState if it should
  }
  void exit() override {
    // Perform any necessary cleanup when exiting this state
  }
};

class CompletedState : public QuestState {
 public:
  void enter() override {
    // Perform any necessary setup when entering this state
  }
  void update() override {
    // No further action is needed, as the quest has already been completed
  }
  void exit() override {
    // Perform any necessary cleanup when exiting this state
  }
};

 

현재 상태에 대한 포인터를 유지하는 퀘스트를 나타내는 클래스를 만들 수 있습니다.

class Quest {
 public:
  Quest() : currentState (std::make_unique<NotStartedState>()) {}
  void update() {
    currentState->update();
  }
  void setState(std::unique_ptr<QuestState> state) {
    currentState->exit();
    currentState = std::move(state);
    currentState->enter();
  }
 private:
  std::unique_ptr<QuestState> currentState;
};

그런 다음 Update 메서드를 호출하여 Quest 클래스의 인스턴스를 만들고 업데이트할 수 있습니다.

 

 

 

Quest quest;
quest.update();

 


 

3. Observer 패턴 예시

 

먼저 퀘스트에 대한 추상 기본 클래스를 정의합니다.

class Quest {
 public:
  virtual void update() = 0;
};


그런 다음 퀘스트 시스템을 나타내는 클래스를 정의합니다. 이 클래스는 퀘스트 목록을 유지하고 특정 이벤트가 발생할 때 이를 알려줍니다.

class QuestSystem {
 public:
  void addQuest(std::shared_ptr<Quest> quest) {
    quests_.push_back(quest);
  }
  void handleEvent(const QuestEvent& event) {
    // 퀘스트 로직
    for (const auto& quest : quests_) {
      quest->update();
    }
  }
 private:
  std::vector<std::shared_ptr<Quest>> quests_;
};


그런 다음 구현할 퀘스트 유형별로 구체적인 클래스를 만들고 퀘스트 시스템에 등록할 수 있습니다.

class KillQuest : public Quest {
 public:
  KillQuest(int targetNpcId) : targetNpcId_(targetNpcId) {}
  void update() override {
    // 퀘스트 로직
  }
 private:
  int targetNpcId_;
};

class CollectQuest : public Quest {
 public:
  CollectQuest(int itemId, int quantity) : itemId_(itemId), quantity_(quantity) {}
  void update() override {
    // 퀘스트 로직
  }
 private:
  int itemId_;
  int quantity_;
};

QuestSystem questSystem;
questSystem.addQuest(std::make_shared<KillQuest>(5));
questSystem.addQuest(std::make_shared<CollectQuest>(10, 3));


그런 다음 퀘스트 시스템에서 handleEvent 메서드를 호출하여 이벤트를 트리거할 수 있습니다.

 

questSystem.handleEvent(QuestEvent::NPC_KILLED);
questSystem.handleEvent(QuestEvent::ITEM_COLLECTED);