게임에서 퀘스트는 방향성을 제공해 주고 게임의 목적성과 재미를 높여주며, 진행 상황을 추적하고 보상을 제공하여 게임을 보다 흥미롭게 만들어줍니다. 퀘스트 시스템을 구현하고 정리하였습니다.

 

 

💬 서론

  • 본 글은 게임을 모두 만든 후에 작성한 글로 핵심 주제 외 클래스 및 다른 기능에 대한 함수가 포함될 수 있습니다.

 

📖 구현 내용

  • 적 처치, 아이템 습득에 대한 퀘스트를 구현합니다.
  • 플레이어는 퀘스트를 받고 퀘스트를 완료할 수 있습니다.
  • 퀘스트 상태를 확인하여 상태별로 이벤트를 처리할 수 있습니다.
  • 스크립터블 오브젝트로 퀘스트의 데이터를 관리합니다.
  • 스크립터블 오브젝트를 프로젝트 폴더 내에서 관리하고, 이 데이터를 자동으로 불러옵니다.
  • ID를 이용하여 고유한 퀘스트의 중복 여부를 검사할 수 있습니다.
  • 하나의 퀘스트에서 여러 개의 목표 중 하나만 수행해도 퀘스트를 완료하는지에 대한 여부를 설정할 수 있습니다.
  • 퀘스트 콘텐츠를 보여주고 현재 진행 시점, 완료한 퀘스트들의 정보를 볼 수 있습니다.

 

⚒️ 구현

  • 이번 글에서는 퀘스트 데이터를 관리하기 위한 디자인을 다룹니다.

· QuestBase

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

namespace Quest
{
    #region 퀘스트 종류

    [System.Serializable]
    public abstract class QuestBase
    {
        [Header("이 값을 표현할 포맷 id번호")] public int formatId = -1;

        /// <summary>
        /// 퀘스트 데이터의 파트 중 하나인 이 퀘스트를 클리어했는가?
        /// </summary>
        [HideInInspector] public bool isPartClear = false;

        abstract public string GetFormatText();
    }

    /// <summary>
    /// 적을 처치해야하는 퀘스트
    /// </summary>
    [System.Serializable]
    public class Quest_KillTargets : QuestBase
    {
        /// <summary>
        /// 처치해야할 대상의 코드
        /// </summary>
        [Header("처치해야할 대상의 코드")][SerializeField] public EnemyCode enemyCode;

        /// <summary>
        /// 처치해야 할 수
        /// </summary>
        [Space(20)][Header("처치해야 할 수")][SerializeField] public int killCount;

        [HideInInspector] public int currentKillCount = 0; //현재 처치한 적 수

        public override string GetFormatText()
        {
            return $"{Mathf.Clamp(currentKillCount, 0, killCount)}/{killCount}";
        }
    }

    /// <summary>
    /// 적을 처치해야하는 퀘스트
    /// </summary>
    [System.Serializable]
    public class Quest_GetItems : QuestBase
    {
        /// <summary>
        /// 획득해야 할 아이템의 코드
        /// </summary>
        [Header("획득해야 할 아이템의 코드")][SerializeField] public EntityCode itemCode;

        /// <summary>
        /// 획득해야 하는 수
        /// </summary>
        [Space(20)][Header("획득해야 하는 수")][SerializeField] public int itemCount;

        [HideInInspector] public int currentItemCount = 0; //현재 획득한 아이템 수

        public override string GetFormatText()
        {
            return $"{Mathf.Clamp(currentItemCount, 0, itemCount)}/{itemCount}";
        }
    }

    #endregion
}

/// <summary>
/// 퀘스트 상태
/// </summary>
[System.Serializable] public enum QuestState
{
    /// <summary>
    /// 단 한번도 퀘스트를 받은적이 없음
    /// </summary>
    NEVER_RECEIVED,

    /// <summary>
    /// 현재 퀘스트를 받아 진행중
    /// </summary>
    ONGOING,

    /// <summary>
    /// 현재 퀘스트를 받은 상태 및 목표를 모두 완수한 상태
    /// </summary>
    CLEAR,

    /// <summary>
    /// 이전에 해당 퀘스트를 완료
    /// </summary>
    CLEARED_PAST
}

 

[System.Serializable]
public abstract class QuestBase
{
    [Header("이 값을 표현할 포맷 id번호")] public int formatId = -1;

    /// <summary>
    /// 퀘스트 데이터의 파트 중 하나인 이 퀘스트를 클리어했는가?
    /// </summary>
    [HideInInspector] public bool isPartClear = false;

    abstract public string GetFormatText();
}
  • 기본적인 퀘스트 데이터셋입니다.
  • 퀘스트의 종류에 따라 다른 속성을 가지는 경우를 고려한 상위 부모 클래스입니다.
  • 함수 오버라이딩을 이용하여 편리하게 이용하기 위해 사용합니다.

 

/// <summary>
/// 적을 처치해야하는 퀘스트
/// </summary>
[System.Serializable]
public class Quest_KillTargets : QuestBase
{
    /// <summary>
    /// 처치해야할 대상의 코드
    /// </summary>
    [Header("처치해야할 대상의 코드")][SerializeField] public EnemyCode enemyCode;

    /// <summary>
    /// 처치해야 할 수
    /// </summary>
    [Space(20)][Header("처치해야 할 수")][SerializeField] public int killCount;

    [HideInInspector] public int currentKillCount = 0; //현재 처치한 적 수

    public override string GetFormatText()
    {
        return $"{Mathf.Clamp(currentKillCount, 0, killCount)}/{killCount}";
    }
}
  • 적을 처치하는 타입의 퀘스트입니다.
  • QuestBase를 상속받고 새로운 속성을 가집니다.
  • GetFromText() 함수를 재정의하여 성질에 맞는 역할을 수행합니다.
  • EnemyCode는 적 객체를 구분하는 enum입니다.

 

/// <summary>
/// 적을 처치해야하는 퀘스트
/// </summary>
[System.Serializable]
public class Quest_GetItems : QuestBase
{
    /// <summary>
    /// 획득해야 할 아이템의 코드
    /// </summary>
    [Header("획득해야 할 아이템의 코드")][SerializeField] public EntityCode itemCode;

    /// <summary>
    /// 획득해야 하는 수
    /// </summary>
    [Space(20)][Header("획득해야 하는 수")][SerializeField] public int itemCount;

    [HideInInspector] public int currentItemCount = 0; //현재 획득한 아이템 수

    public override string GetFormatText()
    {
        return $"{Mathf.Clamp(currentItemCount, 0, itemCount)}/{itemCount}";
    }
}
  • 아이템을 획득하는 퀘스트입니다.
  • 적을 처치하는 퀘스트와 동일하게 QuestBase를 상속받고 새로운 속성을 가집니다.
  • GetFromText() 함수를 재정의하여 성질에 맞는 역할을 수행합니다.
  • EntityCode는 item을 포함한 여러 객체를 구분하는 enum입니다.

 

· QuestData

  • 퀘스트 데이터로 스크립터블 오브젝트를 사용하여 위에서 다룬 클래스들을 멤버변수로 지닙니다.
  • 이 데이터를 이용하여 퀘스트 정보를 읽고 여러 연산을 수행합니다.
[System.Serializable] [CreateAssetMenu(fileName = "Quest", menuName = "Add Quest/Quest")]
public class QuestData : ScriptableObject
{
    [HideInInspector] public QuestState questState = QuestState.NEVER_RECEIVED;

    [Header("고유한 퀘스트의 ID")]
    [SerializeField] public int questId;
    [Space(50)]
    [Header("퀘스트 목표")]
    [SerializeField] public Quest_KillTargets[] killTargetsQuests;//적을 처치하는 목표
    [SerializeField] public Quest_GetItems[] getItemsQuests;//아이템을 획득하는 목표

    [Space(50)]
    [Header("퀘스트의 목표 중 하나만 수행해도 되는 타입인가?")]
    [SerializeField] public bool isOptionalQuestType;//퀘스트의 목표 중 하나만 수행해도 되는 타입인가?

    [Space(10)]
    [Header("퀘스트를 완료 시 지급 경험치")]
    [SerializeField] public float expAmount;// 경험치 양

    [Space(10)]
    [Header("퀘스트를 진행하는 씬 이름")] public string questScene;
    [Header("퀘스트를 의뢰한 대상의 위치")] public Vector3 sourcePos;
    [Header("퀘스트를 수행하러 가야 할 목적지 위치")] public Vector3 destinationPos;

    /// <summary>
    /// 해당 퀘스트가 가지는 모든 부분적 퀘스트
    /// </summary>
    public QuestBase[] allQuests;
}

 

 

[HideInInspector] public QuestState questState = QuestState.NEVER_RECEIVED;
  • 현재 이 퀘스트의 상태를 나타냅니다.
  • 퀘스트를 한 번도 받지 않았는지, 수행 중인지, 완료한 상태인지 등을 저장합니다.

 

[Header("고유한 퀘스트의 ID")]
[SerializeField] public int questId;
  • 퀘스트의 ID입니다.
  • 이 값은 고유하며, 다른 값들과 중복되어서는 안 됩니다.
  • 인스펙터에서 추가 기능으로 ID값이 중복되는지 검사할 수 있습니다.

 

[Header("퀘스트 목표")]
[SerializeField] public Quest_KillTargets[] killTargetsQuests;//적을 처치하는 목표
[SerializeField] public Quest_GetItems[] getItemsQuests;//아이템을 획득하는 목표
  • 퀘스트의 목표를 지정합니다.
  • 위에서 다뤘던 두 개의 퀘스트들을 미리 지정하여 퀘스트 데이터를 구성할 수 있습니다.

 

[Header("퀘스트의 목표 중 하나만 수행해도 되는 타입인가?")]
[SerializeField] public bool isOptionalQuestType;//퀘스트의 목표 중 하나만 수행해도 되는 타입인가?
  • 이 퀘스트가 목표들 중 하나만 수행해도 되는지에 대한 여부를 설정합니다.
  • 이 값이 참이라면, 퀘스트 목표들 중 하나만 완료를 하면 퀘스트 자체를 완료했다고 설정됩니다.

 

[Header("퀘스트를 완료 시 지급 경험치")]
[SerializeField] public float expAmount;// 경험치 양
  • 이 퀘스트를 수행하면 플레이어에게 지급하는 경험치 양을 설정합니다.

 

[Header("퀘스트를 진행하는 씬 이름")] public string questScene;
[Header("퀘스트를 의뢰한 대상의 위치")] public Vector3 sourcePos;
[Header("퀘스트를 수행하러 가야 할 목적지 위치")] public Vector3 destinationPos;
  • 내비게이션 기능을 이용하기 위해 사용하는 변수입니다.
  • questScene은 퀘스트를 수행하는 씬 이름으로, 해당 씬이 아니면 내비게이션이 작동하지 않습니다.
  • 또한 sourcePos, destinationPos는 내비게이션이 향할 위치를 설정합니다.

 

/// <summary>
/// 해당 퀘스트가 가지는 모든 부분적 퀘스트
/// </summary>
public QuestBase[] allQuests;
  • 각 목표들을 상위 클래스인 QuestBase로 저장한 배열입니다.
  • 이 배열을 이용하여 다른 함수들에서 서로 다른 클래스의 퀘스트들을 한 번에 접근할 수 있습니다.

 

/// <summary>
/// 퀘스트 상태
/// </summary>
[System.Serializable] public enum QuestState
{
    /// <summary>
    /// 단 한번도 퀘스트를 받은적이 없음
    /// </summary>
    NEVER_RECEIVED,

    /// <summary>
    /// 현재 퀘스트를 받아 진행중
    /// </summary>
    ONGOING,

    /// <summary>
    /// 현재 퀘스트를 받은 상태 및 목표를 모두 완수한 상태
    /// </summary>
    CLEAR,

    /// <summary>
    /// 이전에 해당 퀘스트를 완료
    /// </summary>
    CLEARED_PAST
}
  • 퀘스트의 상태를 나타냅니다.

 

✅ 사용

  • 스크립터블 오브젝트로 에셋을 만든 후 퀘스트를 설정합니다.
  • 처치해야 할 목표 또는 획득해야 할 아이템 목표를 넣고 설정할 수 있습니다.

 

  • 본 이미지는 늑대를 처치하는 퀘스트를 설정한 결과입니다.
bonnate