Реализовать систему достижений
Условие
Реализуйте систему достижений для игры.
Требования
- ScriptableObject для описания достижения
- Типы: счетчик, разовое событие, коллекция
- Отслеживание прогресса
- Уведомление при получении
- UI панель со списком достижений
- Сохранение полученных достижений
Примеры
- Убить 100 врагов
- Пройти уровень без урона
- Собрать все предметы
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Система достижений для игры
Эта задача требует создания гибкой системы достижений с различными типами отслеживания, сохранением прогресса и UI для отображения. Расскажу о полной архитектуре.
Архитектура системы
Основные компоненты:
- AchievementDefinition — ScriptableObject для описания
- AchievementType — enum типов достижений
- AchievementManager — основной менеджер
- AchievementTracker — отслеживание прогресса
- AchievementUI — интерфейс и уведомления
AchievementDefinition - ScriptableObject
public enum AchievementType
{
Counter, // Счетчик (убить 100 врагов)
OneTime, // Разовое событие (пройти уровень)
Collection // Коллекция (собрать все предметы)
}
[System.Serializable]
public class AchievementProgress
{
public string achievementId;
public bool unlocked = false;
public int progress = 0;
public System.DateTime unlockedDate;
public float completionPercent = 0f;
}
[CreateAssetMenu(fileName = "Achievement_", menuName = "Game/Achievement")]
public class AchievementDefinition : ScriptableObject
{
[SerializeField] public string achievementId;
[SerializeField] public string title;
[TextArea(3, 5)]
[SerializeField] public string description;
[SerializeField] public Sprite icon;
[SerializeField] public AchievementType achievementType;
[SerializeField] public int targetValue = 1; // Для Counter и Collection
[SerializeField] public int rewardPoints = 10;
[SerializeField] public bool isHidden = false; // Скрытое достижение
[SerializeField] public string triggerEventName; // Имя события для OneTime
[SerializeField] public string[] requiredItems; // Для Collection
public bool IsCompleted(AchievementProgress progress)
{
return progress.progress >= targetValue;
}
public float GetProgressPercent(AchievementProgress progress)
{
if (targetValue <= 0)
return 0f;
return Mathf.Min((float)progress.progress / targetValue, 1f);
}
}
AchievementManager - Основной менеджер
public class AchievementManager : MonoBehaviour
{
public static AchievementManager Instance { get; private set; }
[SerializeField] private AchievementDefinition[] achievements;
[SerializeField] private string progressSaveKey = "AchievementProgress";
[SerializeField] private bool autoSave = true;
private Dictionary<string, AchievementProgress> progressMap = new Dictionary<string, AchievementProgress>();
private Dictionary<string, AchievementDefinition> achievementMap = new Dictionary<string, AchievementDefinition>();
private int totalRewardPoints = 0;
public event System.Action<AchievementDefinition, AchievementProgress> OnAchievementUnlocked;
public event System.Action<string, int> OnProgressChanged;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
private void Start()
{
InitializeAchievements();
LoadProgress();
}
private void InitializeAchievements()
{
foreach (var achievement in achievements)
{
if (achievement != null)
{
achievementMap[achievement.achievementId] = achievement;
if (!progressMap.ContainsKey(achievement.achievementId))
{
progressMap[achievement.achievementId] = new AchievementProgress
{
achievementId = achievement.achievementId,
progress = 0,
unlocked = false
};
}
}
}
}
public void IncrementProgress(string achievementId, int amount = 1)
{
if (!progressMap.ContainsKey(achievementId))
return;
if (!achievementMap.TryGetValue(achievementId, out AchievementDefinition definition))
return;
AchievementProgress progress = progressMap[achievementId];
if (progress.unlocked)
return; // Уже получено
progress.progress += amount;
OnProgressChanged?.Invoke(achievementId, progress.progress);
if (definition.IsCompleted(progress))
{
UnlockAchievement(achievementId);
}
if (autoSave)
SaveProgress();
}
public void SetProgress(string achievementId, int value)
{
if (!progressMap.ContainsKey(achievementId))
return;
if (!achievementMap.TryGetValue(achievementId, out AchievementDefinition definition))
return;
AchievementProgress progress = progressMap[achievementId];
progress.progress = value;
OnProgressChanged?.Invoke(achievementId, progress.progress);
if (definition.IsCompleted(progress))
{
UnlockAchievement(achievementId);
}
if (autoSave)
SaveProgress();
}
public void UnlockAchievement(string achievementId)
{
if (!progressMap.ContainsKey(achievementId))
return;
if (!achievementMap.TryGetValue(achievementId, out AchievementDefinition definition))
return;
AchievementProgress progress = progressMap[achievementId];
if (progress.unlocked)
return; // Уже получено
progress.unlocked = true;
progress.unlockedDate = System.DateTime.Now;
progress.completionPercent = 1f;
totalRewardPoints += definition.rewardPoints;
OnAchievementUnlocked?.Invoke(definition, progress);
Debug.Log($"Achievement Unlocked: {definition.title}");
if (autoSave)
SaveProgress();
}
public AchievementProgress GetProgress(string achievementId)
{
progressMap.TryGetValue(achievementId, out AchievementProgress progress);
return progress;
}
public AchievementDefinition GetDefinition(string achievementId)
{
achievementMap.TryGetValue(achievementId, out AchievementDefinition definition);
return definition;
}
public List<AchievementDefinition> GetAllAchievements()
{
return new List<AchievementDefinition>(achievementMap.Values);
}
public int GetUnlockedCount()
{
return progressMap.Values.Count(p => p.unlocked);
}
public int GetTotalRewardPoints() => totalRewardPoints;
public void SaveProgress()
{
List<AchievementProgress> progressList = new List<AchievementProgress>(progressMap.Values);
string json = JsonUtility.ToJson(new AchievementProgressData { achievements = progressList });
PlayerPrefs.SetString(progressSaveKey, json);
PlayerPrefs.Save();
}
public void LoadProgress()
{
if (!PlayerPrefs.HasKey(progressSaveKey))
return;
string json = PlayerPrefs.GetString(progressSaveKey);
try
{
AchievementProgressData data = JsonUtility.FromJson<AchievementProgressData>(json);
foreach (var progress in data.achievements)
{
if (progressMap.ContainsKey(progress.achievementId))
{
progressMap[progress.achievementId] = progress;
// Пересчитываем reward points
if (progress.unlocked && achievementMap.TryGetValue(progress.achievementId, out AchievementDefinition def))
{
totalRewardPoints += def.rewardPoints;
}
}
}
}
catch (System.Exception e)
{
Debug.LogError($"Failed to load achievement progress: {e.Message}");
}
}
public void ResetAllProgress()
{
foreach (var progress in progressMap.Values)
{
progress.unlocked = false;
progress.progress = 0;
}
totalRewardPoints = 0;
SaveProgress();
}
}
[System.Serializable]
public class AchievementProgressData
{
public List<AchievementProgress> achievements = new List<AchievementProgress>();
}
AchievementTracker - Отслеживание событий
public class AchievementTracker : MonoBehaviour
{
[SerializeField] private string enemyDefeatedAchievementId = "kill_100_enemies";
[SerializeField] private string completeLevelAchievementId = "complete_level_no_damage";
[SerializeField] private string collectAllItemsAchievementId = "collect_all_items";
private int currentEnemyKills = 0;
private bool currentLevelDamageTaken = false;
private int itemsCollected = 0;
private int totalItems = 0;
private void Start()
{
// Подписываемся на события врага
Enemy.OnEnemyDefeated += TrackEnemyKill;
// Подписываемся на события здоровья
Health.OnDamageTaken += TrackDamage;
// Подписываемся на события предметов
Collectible.OnItemCollected += TrackItemCollection;
totalItems = FindObjectsOfType<Collectible>().Length;
}
private void OnDestroy()
{
Enemy.OnEnemyDefeated -= TrackEnemyKill;
Health.OnDamageTaken -= TrackDamage;
Collectible.OnItemCollected -= TrackItemCollection;
}
private void TrackEnemyKill()
{
currentEnemyKills++;
AchievementManager.Instance.IncrementProgress(enemyDefeatedAchievementId);
}
private void TrackDamage()
{
currentLevelDamageTaken = true;
}
private void TrackItemCollection()
{
itemsCollected++;
if (itemsCollected >= totalItems)
{
AchievementManager.Instance.UnlockAchievement(collectAllItemsAchievementId);
}
}
public void OnLevelComplete()
{
if (!currentLevelDamageTaken)
{
AchievementManager.Instance.UnlockAchievement(completeLevelAchievementId);
}
ResetLevelData();
}
private void ResetLevelData()
{
currentEnemyKills = 0;
currentLevelDamageTaken = false;
itemsCollected = 0;
}
}
AchievementUI - Интерфейс
public class AchievementUI : MonoBehaviour
{
[SerializeField] private Transform achievementContainer;
[SerializeField] private GameObject achievementItemPrefab;
[SerializeField] private Transform achievementNotificationParent;
[SerializeField] private GameObject notificationPrefab;
[SerializeField] private Text totalPointsText;
[SerializeField] private CanvasGroup achievementPanelGroup;
[SerializeField] private Animator animator;
private Dictionary<string, AchievementItemUI> uiItems = new Dictionary<string, AchievementItemUI>();
private Coroutine notificationCoroutine;
private void Start()
{
AchievementManager.Instance.OnAchievementUnlocked += OnAchievementUnlocked;
AchievementManager.Instance.OnProgressChanged += OnProgressChanged;
DisplayAchievements();
UpdateTotalPoints();
}
private void DisplayAchievements()
{
var achievements = AchievementManager.Instance.GetAllAchievements();
foreach (var achievement in achievements)
{
if (achievement.isHidden)
continue;
GameObject itemObj = Instantiate(achievementItemPrefab, achievementContainer);
AchievementItemUI itemUI = itemObj.GetComponent<AchievementItemUI>();
itemUI.SetAchievement(achievement);
uiItems[achievement.achievementId] = itemUI;
}
}
private void OnAchievementUnlocked(AchievementDefinition achievement, AchievementProgress progress)
{
if (uiItems.TryGetValue(achievement.achievementId, out AchievementItemUI itemUI))
{
itemUI.SetUnlocked();
}
ShowNotification(achievement);
UpdateTotalPoints();
}
private void OnProgressChanged(string achievementId, int progress)
{
if (uiItems.TryGetValue(achievementId, out AchievementItemUI itemUI))
{
AchievementDefinition def = AchievementManager.Instance.GetDefinition(achievementId);
if (def != null)
{
float percent = (float)progress / def.targetValue;
itemUI.UpdateProgress(progress, def.targetValue, percent);
}
}
}
private void ShowNotification(AchievementDefinition achievement)
{
if (notificationCoroutine != null)
StopCoroutine(notificationCoroutine);
GameObject notifObj = Instantiate(notificationPrefab, achievementNotificationParent);
AchievementNotification notification = notifObj.GetComponent<AchievementNotification>();
notification.SetAchievement(achievement);
notificationCoroutine = StartCoroutine(NotificationCoroutine(notifObj));
}
private IEnumerator NotificationCoroutine(GameObject notifObj)
{
yield return new WaitForSeconds(3f);
Destroy(notifObj);
}
private void UpdateTotalPoints()
{
if (totalPointsText != null)
{
totalPointsText.text = $"Очки: {AchievementManager.Instance.GetTotalRewardPoints()}";
}
}
public void TogglePanel()
{
bool isActive = achievementPanelGroup.alpha > 0.5f;
achievementPanelGroup.alpha = isActive ? 0f : 1f;
achievementPanelGroup.blocksRaycasts = !isActive;
}
}
public class AchievementItemUI : MonoBehaviour
{
[SerializeField] private Image iconImage;
[SerializeField] private Text titleText;
[SerializeField] private Text descriptionText;
[SerializeField] private Image progressImage;
[SerializeField] private Text progressText;
[SerializeField] private GameObject lockIcon;
[SerializeField] private Text pointsText;
private AchievementDefinition achievement;
public void SetAchievement(AchievementDefinition achievement)
{
this.achievement = achievement;
if (iconImage != null)
iconImage.sprite = achievement.icon;
if (titleText != null)
titleText.text = achievement.title;
if (descriptionText != null)
descriptionText.text = achievement.description;
if (pointsText != null)
pointsText.text = $"+{achievement.rewardPoints}";
AchievementProgress progress = AchievementManager.Instance.GetProgress(achievement.achievementId);
if (progress != null && progress.unlocked)
{
SetUnlocked();
}
}
public void SetUnlocked()
{
if (lockIcon != null)
lockIcon.SetActive(false);
if (progressImage != null)
progressImage.fillAmount = 1f;
if (progressText != null)
progressText.text = "Получено";
}
public void UpdateProgress(int current, int target, float percent)
{
if (progressImage != null)
progressImage.fillAmount = percent;
if (progressText != null)
progressText.text = $"{current}/{target}";
}
}
public class AchievementNotification : MonoBehaviour
{
[SerializeField] private Image achievementImage;
[SerializeField] private Text achievementText;
[SerializeField] private Animator animator;
public void SetAchievement(AchievementDefinition achievement)
{
if (achievementImage != null)
achievementImage.sprite = achievement.icon;
if (achievementText != null)
achievementText.text = $"Достижение разблокировано!\n{achievement.title}";
if (animator != null)
animator.SetTrigger("Show");
}
}
Ключевые особенности
1. ScriptableObject — описание достижений в инспекторе
2. Три типа достижений — Counter, OneTime, Collection
3. Отслеживание прогресса — система инкрементирования и проверки
4. События — система подписки на события игры
5. Сохранение — PlayerPrefs для хранения прогресса
6. UI Уведомления — всплывающие уведомления при разблокировке
7. Скрытые достижения — достижения которые не видны до разблокировки
Расширение возможностей
- Статистика — отслеживание различных метрик
- Табло лучших результатов — облачное хранилище
- Условные достижения — достижения зависящие от других
- Звуковые эффекты — звук при разблокировке
- Анимации — визуальные эффекты при получении
Эта система обеспечивает полную функциональность для системы достижений любой сложности.