← Назад к вопросам

Реализовать простую систему квестов

2.0 Middle🔥 161 комментариев
#C# и ООП#UI#Unity Core#Паттерны проектирования#Ресурсы и ассеты

Условие

Реализуйте простую систему квестов для RPG игры.

Требования

  1. ScriptableObject для описания квеста
  2. Типы квестов: собрать предметы, убить врагов, дойти до точки
  3. Прогресс квеста (текущее/требуемое)
  4. Награда за выполнение
  5. Цепочки квестов (следующий квест разблокируется после выполнения текущего)
  6. UI панель активных квестов

Структура

  • QuestData (ScriptableObject)
  • Quest (runtime данные)
  • QuestManager (синглтон)
  • QuestUI (отображение)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Архитектура системы квестов

Хорошая система квестов должна быть гибкой, модульной и событийной. Я реализую: QuestData (описание), Quest (runtime), QuestManager (контроль), и QuestUI (отображение).

1. ScriptableObject для квестов

using UnityEngine;
using System.Collections.Generic;

/// <summary>Типы квестов</summary>
public enum QuestType
{
    CollectItems,    // Собрать предметы
    KillEnemies,     // Убить врагов
    ReachLocation,   // Дойти до точки
    TalkToNPC        // Поговорить с NPC
}

/// <summary>Статусы квеста</summary>
public enum QuestStatus
{
    Locked,      // Недоступен
    Active,      // В прогрессе
    Completed,   // Завершён
    Failed       // Провален
}

/// <summary>Награда за квест</summary>
[System.Serializable]
public class QuestReward
{
    public int experiencePoints = 100;
    public int gold = 50;
    public List<string> itemRewards = new List<string>();
}

/// <summary>Задача внутри квеста (для составных квестов)</summary>
[System.Serializable]
public class QuestObjective
{
    public string objectiveId;
    public string description;
    public QuestType type;
    public int targetAmount = 1;
    
    // Зависит от типа квеста
    public string targetItemId;          // Для CollectItems
    public string targetEnemyType;       // Для KillEnemies
    public Vector3 targetLocation;       // Для ReachLocation
    public string targetNPC;             // Для TalkToNPC
}

/// <summary>ScriptableObject с данными квеста</summary>
[CreateAssetMenu(fileName = "Quest_", menuName = "Quest/Quest Data")]
public class QuestData : ScriptableObject
{
    [Header("Basic Info")]
    public string questId;
    public string questName;
    [TextArea(3, 5)]
    public string questDescription;
    [TextArea(2, 3)]
    public string questStory; // Рассказ/контекст
    
    [Header("Objectives")]
    public List<QuestObjective> objectives = new List<QuestObjective>();
    
    [Header("Rewards")]
    public QuestReward reward = new QuestReward();
    
    [Header("Quest Chain")]
    public QuestData nextQuest; // Квест который разблокируется после этого
    public QuestData prerequisiteQuest; // Квест который нужно выполнить до этого
    
    [Header("Settings")]
    public bool isOptional = false;
    public bool canFail = true;
    public float timeLimit = 0f; // 0 = нет ограничения
    
    void OnValidate()
    {
        // Генерируем ID если его нет
        if (string.IsNullOrEmpty(questId))
        {
            questId = name;
        }
    }
}

2. Runtime класс Quest

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

/// <summary>Runtime данные активного квеста</summary>
public class Quest
{
    public QuestData data;
    public QuestStatus status = QuestStatus.Locked;
    public List<QuestObjectiveProgress> objectiveProgress = new List<QuestObjectiveProgress>();
    public float timeElapsed = 0f;
    public bool isCompleted => status == QuestStatus.Completed;
    public bool isFailed => status == QuestStatus.Failed;
    public bool isActive => status == QuestStatus.Active;
    
    // События
    public event Action<Quest> OnQuestStarted;
    public event Action<Quest> OnQuestCompleted;
    public event Action<Quest> OnQuestFailed;
    public event Action<Quest, int> OnObjectiveProgressChanged; // Quest, objectiveIndex
    
    public Quest(QuestData questData)
    {
        data = questData;
        
        // Инициализируем прогресс для каждой задачи
        foreach (var objective in questData.objectives)
        {
            objectiveProgress.Add(new QuestObjectiveProgress
            {
                objectiveId = objective.objectiveId,
                currentAmount = 0,
                targetAmount = objective.targetAmount
            });
        }
    }
    
    public void Start()
    {
        if (status != QuestStatus.Locked)
            return;
        
        status = QuestStatus.Active;
        Debug.Log($"Квест начался: {data.questName}");
        OnQuestStarted?.Invoke(this);
    }
    
    public void UpdateObjectiveProgress(int objectiveIndex, int amount)
    {
        if (objectiveIndex < 0 || objectiveIndex >= objectiveProgress.Count)
            return;
        
        objectiveProgress[objectiveIndex].currentAmount += amount;
        objectiveProgress[objectiveIndex].currentAmount = 
            Mathf.Min(objectiveProgress[objectiveIndex].currentAmount, 
                      objectiveProgress[objectiveIndex].targetAmount);
        
        OnObjectiveProgressChanged?.Invoke(this, objectiveIndex);
        
        // Проверяем завершение всех задач
        if (IsAllObjectivesCompleted())
        {
            Complete();
        }
    }
    
    public bool IsAllObjectivesCompleted()
    {
        foreach (var progress in objectiveProgress)
        {
            if (progress.currentAmount < progress.targetAmount)
                return false;
        }
        return true;
    }
    
    public void Complete()
    {
        if (status == QuestStatus.Completed)
            return;
        
        status = QuestStatus.Completed;
        Debug.Log($"Квест завершён: {data.questName}");
        OnQuestCompleted?.Invoke(this);
    }
    
    public void Fail()
    {
        if (!data.canFail || status != QuestStatus.Active)
            return;
        
        status = QuestStatus.Failed;
        Debug.Log($"Квест провален: {data.questName}");
        OnQuestFailed?.Invoke(this);
    }
    
    public float GetProgressPercent()
    {
        if (objectiveProgress.Count == 0)
            return 0f;
        
        float totalProgress = 0f;
        foreach (var progress in objectiveProgress)
        {
            totalProgress += (float)progress.currentAmount / progress.targetAmount;
        }
        
        return totalProgress / objectiveProgress.Count;
    }
}

/// <summary>Прогресс отдельной задачи</summary>
[System.Serializable]
public class QuestObjectiveProgress
{
    public string objectiveId;
    public int currentAmount;
    public int targetAmount;
    
    public bool IsCompleted => currentAmount >= targetAmount;
    public float ProgressPercent => targetAmount > 0 ? (float)currentAmount / targetAmount : 1f;
}

3. QuestManager

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

public class QuestManager : Singleton<QuestManager>
{
    [Header("Settings")]
    [SerializeField] private int maxActiveQuests = 5;
    [SerializeField] private List<QuestData> allQuests = new List<QuestData>();
    
    // Хранилище квестов
    private Dictionary<string, Quest> questDictionary = new Dictionary<string, Quest>();
    private List<Quest> activeQuests = new List<Quest>();
    private HashSet<string> completedQuestIds = new HashSet<string>();
    
    // События
    public event Action<Quest> OnQuestAdded;
    public event Action<Quest> OnQuestRemoved;
    public event Action<Quest> OnQuestCompleted;
    public event Action<Quest> OnQuestFailed;
    
    protected override void Awake()
    {
        base.Awake();
        InitializeQuests();
    }
    
    void Update()
    {
        // Обновляем время для каждого квеста
        foreach (var quest in activeQuests)
        {
            if (quest.isActive && quest.data.timeLimit > 0)
            {
                quest.timeElapsed += Time.deltaTime;
                if (quest.timeElapsed >= quest.data.timeLimit)
                {
                    quest.Fail();
                }
            }
        }
    }
    
    /// <summary>Инициализируем все квесты</summary>
    private void InitializeQuests()
    {
        foreach (var questData in allQuests)
        {
            Quest quest = new Quest(questData);
            questDictionary[questData.questId] = quest;
            
            // Подписываемся на события квеста
            quest.OnQuestCompleted += OnQuestCompletedHandler;
            quest.OnQuestFailed += OnQuestFailedHandler;
        }
    }
    
    /// <summary>Принять квест</summary>
    public bool AcceptQuest(string questId)
    {
        if (!questDictionary.ContainsKey(questId))
        {
            Debug.LogError($"Квест {questId} не найден");
            return false;
        }
        
        Quest quest = questDictionary[questId];
        
        // Проверяем предварительные условия
        if (quest.data.prerequisiteQuest != null)
        {
            if (!completedQuestIds.Contains(quest.data.prerequisiteQuest.questId))
            {
                Debug.LogWarning($"Требуется выполнить квест {quest.data.prerequisiteQuest.questName}");
                return false;
            }
        }
        
        // Проверяем лимит активных квестов
        if (activeQuests.Count >= maxActiveQuests)
        {
            Debug.LogWarning($"Достигнут лимит активных квестов ({maxActiveQuests})");
            return false;
        }
        
        // Начинаем квест
        quest.Start();
        activeQuests.Add(quest);
        OnQuestAdded?.Invoke(quest);
        
        return true;
    }
    
    /// <summary>Отказать от квеста</summary>
    public bool AbandonQuest(string questId)
    {
        if (!questDictionary.ContainsKey(questId))
            return false;
        
        Quest quest = questDictionary[questId];
        if (quest.isActive)
        {
            quest.Fail();
            return true;
        }
        
        return false;
    }
    
    /// <summary>Обновить прогресс задачи</summary>
    public void UpdateObjectiveProgress(string questId, int objectiveIndex, int amount)
    {
        if (!questDictionary.ContainsKey(questId))
            return;
        
        Quest quest = questDictionary[questId];
        if (quest.isActive)
        {
            quest.UpdateObjectiveProgress(objectiveIndex, amount);
        }
    }
    
    /// <summary>Обновить прогресс по ID задачи</summary>
    public void UpdateObjectiveProgressById(string questId, string objectiveId, int amount)
    {
        if (!questDictionary.ContainsKey(questId))
            return;
        
        Quest quest = questDictionary[questId];
        if (!quest.isActive)
            return;
        
        for (int i = 0; i < quest.data.objectives.Count; i++)
        {
            if (quest.data.objectives[i].objectiveId == objectiveId)
            {
                quest.UpdateObjectiveProgress(i, amount);
                return;
            }
        }
    }
    
    private void OnQuestCompletedHandler(Quest quest)
    {
        activeQuests.Remove(quest);
        completedQuestIds.Add(quest.data.questId);
        OnQuestCompleted?.Invoke(quest);
        
        // Разблокируем следующий квест
        if (quest.data.nextQuest != null)
        {
            Quest nextQuest = questDictionary[quest.data.nextQuest.questId];
            nextQuest.status = QuestStatus.Active; // Делаем доступным
            Debug.Log($"Разблокирован квест: {nextQuest.data.questName}");
        }
    }
    
    private void OnQuestFailedHandler(Quest quest)
    {
        activeQuests.Remove(quest);
        OnQuestFailed?.Invoke(quest);
    }
    
    // Getters
    public List<Quest> GetActiveQuests() => new List<Quest>(activeQuests);
    
    public Quest GetQuest(string questId)
    {
        return questDictionary.ContainsKey(questId) ? questDictionary[questId] : null;
    }
    
    public bool IsQuestCompleted(string questId)
    {
        return completedQuestIds.Contains(questId);
    }
}

4. UI для квестов

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

public class QuestUI : MonoBehaviour
{
    [SerializeField] private Transform questListContainer;
    [SerializeField] private QuestItemUI questItemPrefab;
    [SerializeField] private Canvas questCanvas;
    
    private Dictionary<string, QuestItemUI> activeQuestUIs = new Dictionary<string, QuestItemUI>();
    private QuestManager questManager;
    
    void Start()
    {
        questManager = QuestManager.Instance;
        questManager.OnQuestAdded += DisplayQuest;
        questManager.OnQuestRemoved += RemoveQuest;
        questManager.OnQuestCompleted += OnQuestCompleted;
    }
    
    private void DisplayQuest(Quest quest)
    {
        // Создаём UI элемент для квеста
        QuestItemUI questUI = Instantiate(questItemPrefab, questListContainer);
        questUI.SetQuest(quest);
        activeQuestUIs[quest.data.questId] = questUI;
        
        // Подписываемся на обновления
        quest.OnObjectiveProgressChanged += OnObjectiveProgressChanged;
    }
    
    private void RemoveQuest(Quest quest)
    {
        if (activeQuestUIs.ContainsKey(quest.data.questId))
        {
            Destroy(activeQuestUIs[quest.data.questId].gameObject);
            activeQuestUIs.Remove(quest.data.questId);
        }
    }
    
    private void OnQuestCompleted(Quest quest)
    {
        if (activeQuestUIs.ContainsKey(quest.data.questId))
        {
            activeQuestUIs[quest.data.questId].MarkCompleted();
        }
    }
    
    private void OnObjectiveProgressChanged(Quest quest, int objectiveIndex)
    {
        if (activeQuestUIs.ContainsKey(quest.data.questId))
        {
            activeQuestUIs[quest.data.questId].UpdateObjectiveUI(objectiveIndex);
        }
    }
}

/// <summary>UI элемент для одного квеста</summary>
public class QuestItemUI : MonoBehaviour
{
    [SerializeField] private Text questNameText;
    [SerializeField] private Image progressBar;
    [SerializeField] private Transform objectivesContainer;
    [SerializeField] private Text objectiveItemPrefab;
    [SerializeField] private Button abandonButton;
    
    private Quest quest;
    private List<Text> objectiveTexts = new List<Text>();
    
    public void SetQuest(Quest q)
    {
        quest = q;
        questNameText.text = quest.data.questName;
        
        // Создаём UI для каждой задачи
        foreach (var objective in quest.data.objectives)
        {
            Text objectiveText = Instantiate(objectiveItemPrefab, objectivesContainer);
            objectiveText.text = objective.description;
            objectiveTexts.Add(objectiveText);
        }
        
        abandonButton.onClick.AddListener(() => 
            QuestManager.Instance.AbandonQuest(quest.data.questId));
        
        UpdateUI();
    }
    
    public void UpdateObjectiveUI(int objectiveIndex)
    {
        if (objectiveIndex < objectiveTexts.Count)
        {
            var progress = quest.objectiveProgress[objectiveIndex];
            objectiveTexts[objectiveIndex].text = 
                $"{quest.data.objectives[objectiveIndex].description}: {progress.currentAmount}/{progress.targetAmount}";
        }
        
        UpdateUI();
    }
    
    public void MarkCompleted()
    {
        questNameText.color = Color.green;
        progressBar.fillAmount = 1f;
    }
    
    private void UpdateUI()
    {
        progressBar.fillAmount = quest.GetProgressPercent();
    }
}

Пример использования

public class NPC : MonoBehaviour
{
    [SerializeField] private QuestData questData;
    
    public void StartQuest()
    {
        QuestManager.Instance.AcceptQuest(questData.questId);
    }
}

public class EnemyKill : MonoBehaviour
{
    [SerializeField] private string enemyType = "Goblin";
    
    public void Die()
    {
        QuestManager.Instance.UpdateObjectiveProgressById(
            "kill_enemies_quest",
            "kill_10_goblins",
            1
        );
    }
}

Ключевые особенности

1. ScriptableObject — переиспользуемые конфиги квестов.

2. Цепочки — prerequisiteQuest и nextQuest для чейнов.

3. Задачи — множественные объективы в одном квесте.

4. События — система уведомления для других систем.

5. Награды — опыт, золото и предметы при завершении.

6. Время — опциональный лимит времени на квест.

Эта система обеспечивает гибкость, расширяемость и удобство для создания квестовой системы в RPG.

Реализовать простую систему квестов | PrepBro