← Назад к вопросам
Реализовать простую систему квестов
2.0 Middle🔥 161 комментариев
#C# и ООП#UI#Unity Core#Паттерны проектирования#Ресурсы и ассеты
Условие
Реализуйте простую систему квестов для RPG игры.
Требования
- ScriptableObject для описания квеста
- Типы квестов: собрать предметы, убить врагов, дойти до точки
- Прогресс квеста (текущее/требуемое)
- Награда за выполнение
- Цепочки квестов (следующий квест разблокируется после выполнения текущего)
- 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.