Система внутриигровых достижений

Внутриигровые достижения или «ачивки», выдаются игроку за выполнения различных заданий, которые были заботливо подготовлены, воображением разработчика, в порывах творческого экстаза. Для организации ачивок в игре, нам нужно выводить общий список всех возможных достижений. Каждая ачивка списка, должна иметь: уникальную иконку, заголовок, описание, прогресс бар и текст, информирующий о текущем прогрессе в числовом варианте. Показ ачивки, которая была открыта в данный момент, осуществляется через отдельный шаблон. Дополнительно, нам нужно еще учитывать, что игрок может открыть одновременно несколько достижений, поэтому необходимо встроить функцию, которая будет показывать ачивки по очереди. Кроме того, не стоит забывать о сохранении прогресса достижений.


Для начала, нужно сделать пару шаблонов.

Первый шаблон для вывода текущего достижения:

Система внутриигровых достижений

На этот шаблон вешаем скрипт:

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

public class AchievementComponent : MonoBehaviour {

	[SerializeField] private Image _icon;
	[SerializeField] private Text _title;
	[SerializeField] private Text _description;

	public bool isActive { get; set; }

	void Show()
	{
		gameObject.SetActive(true);

		StartCoroutine(Wait(5)); // скрыть через 5 секунд
	}

	void Hide()
	{
		gameObject.SetActive(false);

		AchievementSystem.use.ShowNextAchievement(); // показать следующую открытую ачивку, если таковая есть
	}
	
	public void SetAchievement(Sprite icon, string title, string description)
	{
		_icon.sprite = icon;
		_title.text = title;
		_description.text = description;
		isActive = true;
		Show();
	}

	IEnumerator Wait(float t)
	{
		yield return new WaitForSeconds(t);
		Hide();
	}
}

Вывод иконки, заголовка и описания. Плюс использование функции поочередного показа.

Шаблон для списка несколько отличается:


Соответственно и скрипт тоже другой:

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

public class AchievementListComponent : MonoBehaviour {

	[SerializeField] private int _achievementID;
	[SerializeField] private RectTransform _rectTransform;
	[SerializeField] private Image _icon;
	[SerializeField] private Text _title;
	[SerializeField] private Text _description;
	[SerializeField] private Slider _progressBar;
	[SerializeField] private Text _progressText;

	public int achievementID
	{
		get{ return _achievementID; }
	}

	public RectTransform rectTransform
	{
		get{ return _rectTransform; }
	}

	public void CreateAchievement(int id, string title, string description)
	{
		_achievementID = id;
		_title.text = title;
		_description.text = description;
	}

	public void SetAchievement(Sprite icon, int targetValue, int currentValue)
	{
		_icon.sprite = icon;
		_progressText.text = currentValue + "/" + targetValue;
		_progressBar.value = (float)currentValue/(float)targetValue;
	}
}

Здесь выводиться та же информация, что и в первом шаблоне, но плюс к этому еще прогресс бар.

Эти шаблоны мы создаем на новом Canvas и на него же, вешаем класс:

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

public class AchievementSystem : MonoBehaviour {

	[Header("Редактирование:")]
	[SerializeField] private Achievement[] achievements; // настраиваемый вручную список ачивок
	[Header("Шаблоны:")]
	[SerializeField] private AchievementComponent messageSample; // шаблон для ачивки, которая показывается по достижению
	[SerializeField] private AchievementListComponent listSample; // шаблон для ачивки, которая генерируется в список
	[Header("Настройки:")]
	#if UNITY_EDITOR
	[SerializeField] private float offset = 10; // смещение между ачивками в списке
	#endif
	[SerializeField] private RectTransform listTransform; // трансформ, который будет содержать список
	[SerializeField] private AchievementListComponent[] list; // генерируемый в редакторе список (обязательная сериализация)

	private static AchievementSystem _internal;
	private static bool _active;
	public delegate void MethodOnAchievement(int id, string title, string description);
	public event MethodOnAchievement OnAchievement;
	private List<Achieve> achieveLast;

	[System.Serializable] struct Achievement
	{
		public bool isAchieved; // ачивка открыта или нет
		public string title; // заголовок, название
		public string description; // описание ачивки
		public int targetValue; // значение, которое надо получить, чтобы открыть ачивку
		public int currentValue; // текущее значение, достигнутое пользователем
		public Sprite locked; // спрайт, когда ачивка заблокирована
		public Sprite unlocked; // спрайт, если ачивка разблокирована
	}

	public static AchievementSystem use
	{
		get{ return _internal; }
	}

	public static bool isActive
	{
		get{ return _active; }
	}

	void Awake()
	{
		achieveLast = new List<Achieve>();
		_active = false;
		_internal = this;
		listSample.gameObject.SetActive(false);
		messageSample.gameObject.SetActive(false);
		listTransform.gameObject.SetActive(false);
		Load();
	}

	public void Save()
	{
		string content = string.Empty;

		foreach(Achievement achieve in achievements)
		{
			if(content.Length > 0) content += "|";
			if(achieve.isAchieved) content += achieve.isAchieved.ToString(); else content += achieve.currentValue.ToString();
		}

		PlayerPrefs.SetString("Achievements", content);
		PlayerPrefs.Save();
		Debug.Log(this + " сохранение прогресса ачивок.");
	}

	void Load()
	{
		if(!PlayerPrefs.HasKey("Achievements")) return;

		string[] content = PlayerPrefs.GetString("Achievements").Split(new char[]{'|'});

		if(content.Length == 0 || content.Length != achievements.Length) return;

		for(int i = 0; i < achievements.Length; i++)
		{
			int j = Parse(content[i]);

			if(j < 0)
			{
				achievements[i].currentValue = achievements[i].targetValue;
				achievements[i].isAchieved = true;
			}
			else
			{
				achievements[i].currentValue = j;
			}
		}
	}

	int Parse(string text)
	{
		int value;
		if(int.TryParse(text, out value)) return value;
		return -1;
	}

	public void ShowAchievementList(bool value)
	{
		if(value) // обновление списка, перед показом
		{
			for(int i = 0; i < achievements.Length; i++)
			{
				Sprite sprite = (achievements[i].isAchieved) ? achievements[i].unlocked : achievements[i].locked;
				list[i].SetAchievement(sprite, achievements[i].targetValue, achievements[i].currentValue);
			}
		}

		_active = value;
		listTransform.gameObject.SetActive(value);
	}

	// id - индекс ачивки из списка
	// value - на сколько пунктов изменить
	public void AdjustAchievement(int id, int value)
	{
		if(achievements[id].isAchieved || id < 0 || id > achievements.Length) return;

		achievements[id].currentValue += value;

		if(achievements[id].currentValue < 0) achievements[id].currentValue = 0;

		if(achievements[id].currentValue >= achievements[id].targetValue)
		{
			achievements[id].currentValue = achievements[id].targetValue;
			achievements[id].isAchieved = true;
			OnAchievement(id, achievements[id].title, achievements[id].description);

			if(!messageSample.isActive) // показываем ачивку, если в данный момент не показывается
			{
				messageSample.SetAchievement(achievements[id].unlocked, achievements[id].title, achievements[id].description);
			}
			else // или запоминаем ачивку, чтобы показать позже
			{
				Achieve a = new Achieve();
				a.description = achievements[id].description;
				a.title = achievements[id].title;
				a.sprite = achievements[id].unlocked;
				achieveLast.Add(a);
			}
		}
	}

	struct Achieve
	{
		public string title;
		public string description;
		public Sprite sprite;
	}

	public void ShowNextAchievement() // поочередный показ ачивок, если было открыта сразу несколько
	{
		int j = -1;
		for(int i = 0; i < achieveLast.Count; i++)
		{
			j = i;
		}

		if(j < 0)
		{
			messageSample.isActive = false;
			return;
		}

		messageSample.SetAchievement(achieveLast[j].sprite, achieveLast[j].title, achieveLast[j].description);
		achieveLast.RemoveAt(j);
	}

	#if UNITY_EDITOR
	public void CreateInEditor() // инструмент для создания списка
	{
		foreach(AchievementListComponent e in list)
		{
			if(e) DestroyImmediate(e.gameObject);
		}
		float step = listSample.rectTransform.sizeDelta.y + offset;
		float sizeY = step * achievements.Length;
		listTransform.sizeDelta = new Vector2(listSample.rectTransform.sizeDelta.x, sizeY);
		float posY = step * achievements.Length/2 + step/2;
		list = new AchievementListComponent[achievements.Length];
		for(int i = 0; i < achievements.Length; i++)
		{
			posY -= step;
			RectTransform tr = Instantiate(listSample.rectTransform) as RectTransform;
			tr.SetParent(listTransform);
			tr.localScale = Vector3.one;
			tr.anchoredPosition = new Vector2(0, posY);
			tr.gameObject.SetActive(true);
			tr.name = "Achievement_" + i;
			list[i] = tr.GetComponent<AchievementListComponent>();
			list[i].CreateAchievement(i, achievements[i].title, achievements[i].description);
		}
	}
	#endif
}

Данный класс является управляющим, он контролирует шаблоны, создает ачивки и к нему мы обращаемся, чтобы изменить прогресс достижений. Сохранение и загрузка сделаны через систему PlayerPrefs, используется всего лишь один параметр, так как вся информация записывается в одну строку. Стоит обратить внимание, что обновление списка происходит не в реальном времени, а только когда игрок его открывает.

Создаются достижения в панели редактора:


А затем, кликаем на кнопочку "генерировать" и список достижений будет готов.

Вспомогательный класс для редактора:

#if UNITY_EDITOR
using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor(typeof(AchievementSystem))]

public class AchievementSystemEditor : Editor {

	public override void OnInspectorGUI()
	{
		DrawDefaultInspector();
		AchievementSystem e = (AchievementSystem)target;
		GUILayout.Label("Генерировать список ачивок:", EditorStyles.boldLabel);
		if(GUILayout.Button("Create / Update"))
		{
			e.CreateInEditor();
		}
	}
}
#endif

Закидываем его в папку со скриптами проекта.

Использование.

Чтобы открывать/закрывать список из другого класса:

void Update()
{
	if(Input.GetKeyDown(KeyCode.Space) && !AchievementSystem.isActive)
	{
		AchievementSystem.use.ShowAchievementList(true);
	}
	else if(Input.GetKeyDown(KeyCode.Space) && AchievementSystem.isActive)
	{
		AchievementSystem.use.ShowAchievementList(false);
	}
}

Чтобы сохранить достижения, допустим, когда игрок дойдет до чекпоинта:

AchievementSystem.use.Save();

Чтобы изменить ачивку:

AchievementSystem.use.AdjustAchievement(id, value);

Здесь мы отправляем айди достижения, к которому хотим обратиться (айди берется из списка). А второе это значение, на сколько пунктов изменить, добавить или даже убавить, текущий прогресс. Например, если в ачивке параметр targetValue = 5 то, если мы отправляем value = 1 нам нужно сделать это пять раз, таким образом, значения, targetValue и currentValue, нашего достижения станут равны. И игроку будет выдано сообщение, что ачивка открыта. В общем, схема использования довольно простая.

Скачать демо проект:

Внимание! Посетители, находящиеся в группе Гости, не могут скачивать файлы.
Тестировалось на: Unity 5.4.0

Комментариев 4

Офлайн
Light 1 апреля 2017
CompanyBolt, просто добавить звук в функцию AdjustAchievement.
Офлайн
CompanyBolt 31 марта 2017
Привет, классно получилось, а можно как-то сделать: что при выполнение ачивки воспроизводился звук? По звуку будет видно что ты получил ачивку, да и интересно будет)
Офлайн
Slava XD 6 сентября 2016
привет тоже слежу за каждым выпуском красава что делаешь такие уроки и есть просьба по теме неучи пж делать систему прокачки перса
Офлайн
DropDeadRed 1 сентября 2016
Хочу Вас поблагодарить за Вашу отзывчивость. Здорово, что пожелания пользователей не остаются незамеченными. И жалко что такой годный ресурс особо не пользуется спросом. Хотя, с другой стороны, его это делает невероятно ламповым. В общем, спасибо)
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика