Меню выбора сцены с сохранением

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


UI меню состоит из пары кнопок "вперед" и "назад".
Еще нужно создать группу кнопок, для выбора сцены.

На каждую кнопку в группе, вешается:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEngine.SceneManagement;
using System.IO;

public class LevelSelect : MonoBehaviour {

	[SerializeField] private string iconPath = "SceneIcons"; // папка в Resources, где лежат уникальные иконки сцен, их имена должны быть как и у сцен (необязательно)
	[SerializeField] private string fileName = "Levels.data"; // файл для сохранения
	[SerializeField] private string scenePrefix = "Scene_"; // приставка имени сцены, например, может быть так: Scene_1, Scene_2, Scene_3 и т.п.
	[SerializeField] private KeyCode key; // клавиша вкл/выкл меню
	[SerializeField] private GameObject levelMenu; // родительский объект менюшки
	[SerializeField] private RectTransform levelGroup; // группа иконок
	[SerializeField] private int groupCount = 5; // сколько страниц, формула: например, группа иконок содержит 10 объектов, то умножаем на количество страниц = число сцен
	[SerializeField] private Button backButton; // предощущая страница
	[SerializeField] private Button nextButton; // следующая страница
	[SerializeField] private Sprite lockIcon; // иконка, если сцена закрыта
	[SerializeField] private Sprite unlockIcon; // иконка, если сцена открыта
	[SerializeField] private bool dontDestroyOnLoad; // если 'true' - менюшка будет переходить из сцены в сцену
	[SerializeField] private bool hideOnStart = true; // спрятать меню на старте

	private static bool _active;
	private static LevelSelect _internal;
	private static int groupIndex, current;
	private LevelSelectComponent[] comp;
	private LevelData[] data;
	private Sprite[] sceneIcon;

	struct LevelData // параметры для сохранения
	{
		public bool isActive, canUse; // обязательные

		// пользовательские (можно изменять)
		public int score;
		public string time;
	}

	public void SaveScene(int score, string time)
	{
		if(current <= data.Length-1)
		{
			data[current].isActive = true;
			data[current].score = score;
			data[current].time = time;
		}

		StreamWriter writer = new StreamWriter(GetPath());

		foreach(LevelData element in data)
		{
			if(element.isActive)
			{
				writer.WriteLine(element.score + "|" + element.time);
			}
		}

		writer.Close();

		Debug.Log(this + " Cохранение в файл: " + GetPath());

		if(current+1 <= data.Length-1) // после сохранения, открываем следующую сцену, если таковая есть
		{
			data[current+1].canUse = true;
			ButtonUpdate();
		}
	}

	public void LoadNext()
	{
		if(current+1 <= data.Length-1) LoadScene(current+1);
	}

	void SetLevel(string text, int index)
	{
		string[] t = text.Split(new char[]{'|'});

		// загрузка в таком же порядке, что и запись
		int score = Parse(t[0]);
		string time = t[1];

		data[index].isActive = true;
		data[index].score = score;
		data[index].time = time;
	}

	void Load()
	{
		backButton.interactable = false;

		if(!File.Exists(GetPath())) // если файла сохранения еще нет, то будет открыта первая сцена
		{
			if(data.Length > 0)
			{
				data[0].canUse = true;
				ButtonUpdate();
			}
			return;
		}

		StreamReader reader = new StreamReader(GetPath());

		int j = -1;
		while(!reader.EndOfStream)
		{
			j++;
			SetLevel(reader.ReadLine(), j);
		}

		if(j <= data.Length-1)
		{
			data[j+1].canUse = true;
		}

		reader.Close();

		ButtonUpdate();
	}

	public void LoadScene(int id)
	{
		string level = scenePrefix + id;

		if(!Application.CanStreamedLevelBeLoaded(level))
		{
			Debug.Log("[LevelSelect] сцены не существует или она не добавлена в Build Setting: " + level);
			return;
		}

		Hide();

		current = id;

		SceneManager.LoadScene(level);
	}

	public bool isComplete
	{
		get{ return data[current].isActive; }
	}

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

	public static int Current
	{
		get{ return current; }
	}

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

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

	void Awake()
	{
		_internal = this;
		if(dontDestroyOnLoad) DontDestroyOnLoad(transform.gameObject);
		sceneIcon = Resources.LoadAll<Sprite>(iconPath);
		groupIndex = 1;
		if(hideOnStart) Hide();
		backButton.onClick.AddListener(() => {Back();});
		nextButton.onClick.AddListener(() => {Next();});
		comp = levelGroup.GetComponentsInChildren<LevelSelectComponent>();
		data = new LevelData[comp.Length * groupCount];
		Load();
	}

	string GetPath()
	{
		return Application.persistentDataPath + "/" + fileName;
	}

	void Show()
	{
		_active = true;
		levelMenu.SetActive(true);
	}

	void Hide()
	{
		_active = false;
		levelMenu.SetActive(false);
	}

	void Back()
	{
		if(groupIndex > 1)
		{
			nextButton.interactable = true;
			groupIndex--;
			ButtonUpdate();
		}

		if(groupIndex == 1) backButton.interactable = false;
	}

	void Next()
	{
		if(groupIndex < groupCount)
		{
			backButton.interactable = true;
			groupIndex++;
			ButtonUpdate();
		}

		if(groupIndex == groupCount) nextButton.interactable = false;
	}

	void LateUpdate()
	{
		if(Input.GetKeyDown(key) && !_active) Show();
		else if(Input.GetKeyDown(key) && _active) Hide();
	}

	Sprite GetSprite(bool isLock, string iconName)
	{
		if(isLock) return lockIcon;

		if(sceneIcon.Length > 0)
		{
			foreach(Sprite element in sceneIcon)
			{
				if(string.Compare(element.name, iconName) == 0)
				{
					return element;
				}
			}
		}

		return unlockIcon;
	}

	void ButtonUpdate()
	{
		int j = (comp.Length * groupIndex) - comp.Length;
		foreach(LevelSelectComponent element in comp)
		{
			if(data[j].isActive || data[j].canUse)
			{
				element.button.interactable = true;
				element.button.image.sprite = GetSprite(false, scenePrefix + j);
			}
			else
			{
				element.button.interactable = false;
				element.button.image.sprite = GetSprite(true, scenePrefix + j);
			}

			element.id = j;

			j++;

			element.buttonText.text = j.ToString();
		}
	}
}

Обратить внимание стоит на функции сохранения и загрузки. Там есть обязательные параметры, которые нельзя изменять. А вот пользовательские уже можно видоизменить или добавить свои, следуя примеру, как это сделано с дефолтными. В самом файле сохранения, каждая строка, считается как открытый уровень, если считать по порядку.

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

if(!LevelSelect.use.isComplete)
{
	// проверка, пройден текущий уровень или нет
}

LevelSelect.use.SaveScene(очки, "время"); // сохранить

LevelSelect.use.LoadNext(); // загрузить следующую сцену

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

У вас нет доступа!
Тестировалось на: Unity 5.4.0

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

Офлайн
Антохич 4 ноября 2016
А как сделать так,чтобы при прохождении уровня открывался след. уровень?
Офлайн
Light 5 ноября 2016
Антохич, тут так и сделано, если в конце уровня выполнить SaveScene, то будет открыт следующий.
Офлайн
развернул пакет, но что то как то не пашет.
до "запуска" ещё видно менюшку 4х4 с кнопками с цифрой (100), но при "запуске" сцены Demo - просто серый экран.

Так и должно быть (я что то не настроил?) или на юньке 5.5.1 не работает?
Офлайн
Light 8 февраля 2017
Марк Никулин, нужно настроить клавишу вкл/выкл в инспекторе, переменная Key.
Офлайн
Цитата: Light
Марк Никулин, нужно настроить клавишу вкл/выкл в инспекторе, переменная Key.

а это в каком скрипте на каком объекте? А то упрлс и в упор не вижу такой переменной -.-
Офлайн
Light 9 февраля 2017
Марк Никулин, класс LevelSelect, в скриптах есть комменты.
Офлайн
а вопрос скорее риторический но всё же интересно:
а то что при вызове метода "SaveScene" скрипт добавляет новое сохранение вместо того что бы "перезаписывать" старое - это так и задумано было?)

(другими словами если запускать первый "уровень" то в конце его будет открываться 2-й, 3-й и т.д :O)
Офлайн
подскажите пожалуйста, как реализовать считывание "score" и "time" из файла с сохранениями?

т.е. например мне нужно что бы в меню в переменную ScoreScene1 записывалаись данные из первой строки документа с сохранениями, и т.д. bowtie
Офлайн
Light 28 февраля 2017
Марк Никулин, для этого там есть метод Load.
Офлайн
Mari4og 17 мая 2017
Добрый день. Подскажите пож., как активировать канвас с левел меню (ваш префаб, который я положил в игровую сцену) из моего меню паузы, при нажатии на кнопку Load? Сейчас у меня LoadMenu в отдельной сцене:
public void Load()
{
SceneManager.LoadScene(0);
}
И соответственно, как правильно исправить в вашем скрипте, что бы вызов был не по L, а с кнопки меню паузы?
void LateUpdate()
{
if(Input.GetKeyDown(key) && !_active) Show();
else if(Input.GetKeyDown(key) && _active) Hide();
}
И мне интересно, можно ли, вообще, реализовать все меню(Мэйн меню, меню паузы, левел меню) внутри игровых левелов на разных канвасах, переключаясь между ними?
Спасибо.
Офлайн
Light 17 мая 2017
Mari4og, в настройка скрипта есть отдельная переменная:

[SerializeField] private KeyCode key; // клавиша вкл/выкл меню

Где можно выбрать любую клавишу для вызова меню.

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

Цитата: Mari4og
И мне интересно, можно ли, вообще, реализовать все меню(Мэйн меню, меню паузы, левел меню) внутри игровых левелов на разных канвасах, переключаясь между ними?

Это бессмысленная трата ресурсов системы и собственных сил. Лучше всего делать все менюшки игры и HUD на одном Canvas, создать дочерние группы и переключаться между этих групп.

Например, для своих игр, я написал простой скрипт, типо менеджер менюшек:

using UnityEngine;
using System.Collections;

public class MenuManager : MonoBehaviour {

	[SerializeField] private GameObject[] menu;
	private static MenuManager _use;

	void Awake()
	{
		_use = this;
	}

	public static void ClearMenu()
	{
		_use.SetClear();
	}

	public static void ActiveMenu(string value)
	{
		_use.SetActiveMenu(value);
	}

	public static void ActiveMenuAdd(string value)
	{
		_use.SetActiveMenuAdd(value);
	}

	void SetActiveMenuAdd(string value)
	{
		foreach(GameObject e in menu)
		{
			if(string.Compare(e.name, value) == 0)
			{
				e.SetActive(true);	
			}
		}
	}

	void SetClear()
	{
		foreach(GameObject e in menu)
		{
			e.SetActive(false);
		}
	}

	void SetActiveMenu(string value)
	{
		foreach(GameObject e in menu)
		{
			e.SetActive(false);
		}

		foreach(GameObject e in menu)
		{
			if(string.Compare(e.name, value) == 0)
			{
				e.SetActive(true);	
			}
		}
	}
}

В массив menu нужно добавить все дочерние группы Canvas, то есть менюшки.

Чтобы сделать какое-нибудь меню активным, делаем вызов:

MenuManager.ActiveMenu("имя");

Нужно указать имя, которое есть в массиве.

А если меню уже открыто, но надо добавить еще одно, то:

MenuManager.ActiveMenuAdd("имя");

Удобно и просто.
Офлайн
Mari4og 17 мая 2017
Огромное спасибо за ответ. Я, правда, не программист, совсем. Но попробую разобраться.
Офлайн
Light, а куда в кнопках как бы ссылку на сцену вставлять то?Например нажал 1 и загрузился уровень 1 нажал на 2 после прохождения загрузился второй
Офлайн
Давуд Ахмедов,
самому можно написать:

сразу скажу что могу допустить ошибки, но по идее всё работает, немного попотеть и можно будет сделать так как тебе захочется :)
Офлайн
Цитата: Андрей Пивень
Давуд Ахмедов,
самому можно написать:

сразу скажу что могу допустить ошибки, но по идее всё работает, немного попотеть и можно будет сделать так как тебе захочется :)

спасибо!!!;)
Офлайн
Цитата: Андрей Пивень
Давуд Ахмедов,
самому можно написать:

сразу скажу что могу допустить ошибки, но по идее всё работает, немного попотеть и можно будет сделать так как тебе захочется :)

Но я просто имел ввиду есть ли место в скрипте куда можно было ссылку на уровень вставить
например кнопка 1 в меню
поставил ссылку уровня
нажал на кнопку и уровень загрузился
Офлайн
Давуд Ахмедов,
понял, тогда строчить придётся меньше, но проделать некоторые действия придётся.
выставляем этот скрипт на любой (желательно пустой) объект.

потом добавляем на сцену кнопку (GameObject/UI/Button), после в этой кнопке будет компонент "Button" и в нём функция On Click() в нижнем углу будет +, нажимаем и добавляем туда наш объект в который мы занесли наш скрипт, добавляем нашу функцию как на картинке:
http://savepic.ru/14372713.png
Запускаем, радуемся, повторюсь что могут быть ошибки )
Прошу прощения за плохую картинку.
Так же, таким же способом и немного подкорректировав скрипт запускаем следующие сцены, можно ещё некоторые условия добавить, что можно будет запустить только если пройдена предыдущая сцена.
Офлайн
Light,
Не работает у меня Unity 5.5.1 после прохождения сцены следующая не открывается файл сохранения есть кнопки все есть.ВСЕ СДЕЛАЛ КАК НА УРОКЕ,но не открывается след уровень сцены кстати тоже в Build Setings добавлены.Что делать?
И да зачем скрипт TestTest нужен?
Офлайн
Она даже не загружает 1 уровня при нажатии
Офлайн
Light 17 июня 2017
Давуд Ахмедов, в примере TestTest показано, как сохранять, чтобы открывалась следующая сцена.
Офлайн
Light,
Так почему у меня не работает если все без ошибки правильно?
Офлайн
Light 17 июня 2017
Давуд Ахмедов, значит где-то ошибся, проверяй, может что-то пропустил.
Офлайн
Light,
да нету ошибок я все пересматривал по 40 раз ну нету ошибок нету
Офлайн
А стоп подожди ка а TestTest нужно в Canvas?
Офлайн
Light 17 июня 2017
Давуд Ахмедов, я этот скрипт использую для своих игр на андройде, и вообще не публикую не рабочие скрипты.
Офлайн
Light,
У меня все сработало но почему когда у меня появляется Меню выбора уровня открывается след уровень как это исправить?
Офлайн
Light 17 июня 2017
Давуд Ахмедов, следующий уровень открывается только после выполнения функции SaveScene
Офлайн
Light,
я сделал так чтобы после коссания определенного объекта в моем случаи endLevel открывался новый уровень все работает.
Но есть 1 баг после прохождения любого уровня если например открыто 7 уровней а я прошел 1 уровень у меня открывается 8 уровень как это справить?
Как сделать чтобы после прохождения пройденного уровня ничего не открывалось,а после прохождения последнего открытого уроня только тогда открывался следующий?
Офлайн
Light 17 июня 2017
Давуд Ахмедов, для этого нужно делать проверку перед сохранением, я внес изменения в скрипт. Скачай заново пакет и посмотри TestTest.
Офлайн
Light,
Ошибку дает сказано LevelSelect не содержит определения для IsConplete
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Дешевый хостинг
  • Яндекс.Метрика