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

Реализовать систему достижений

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

Условие

Реализуйте систему достижений для игры.

Требования

  1. ScriptableObject для описания достижения
  2. Типы: счетчик, разовое событие, коллекция
  3. Отслеживание прогресса
  4. Уведомление при получении
  5. UI панель со списком достижений
  6. Сохранение полученных достижений

Примеры

  • Убить 100 врагов
  • Пройти уровень без урона
  • Собрать все предметы

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

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

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

Решение: Система достижений для игры

Эта задача требует создания гибкой системы достижений с различными типами отслеживания, сохранением прогресса и UI для отображения. Расскажу о полной архитектуре.

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

Основные компоненты:

  1. AchievementDefinition — ScriptableObject для описания
  2. AchievementType — enum типов достижений
  3. AchievementManager — основной менеджер
  4. AchievementTracker — отслеживание прогресса
  5. 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. Скрытые достижения — достижения которые не видны до разблокировки

Расширение возможностей

  • Статистика — отслеживание различных метрик
  • Табло лучших результатов — облачное хранилище
  • Условные достижения — достижения зависящие от других
  • Звуковые эффекты — звук при разблокировке
  • Анимации — визуальные эффекты при получении

Эта система обеспечивает полную функциональность для системы достижений любой сложности.