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

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


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

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

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

public class LevelSelectComponent : MonoBehaviour {

	[SerializeField] private Button _button;
	[SerializeField] private Text _buttonText;

	public int id { get; set; }

	public Text buttonText
	{
		get{ return _buttonText; }
	}

	public Button button
	{
		get{ return _button; }
	}

	public void ButtonAction() // событие кнопки
	{
		LevelSelect.use.LoadScene(id);
	}
}

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

Указываем саму кнопку, ее текст и добавляем событие.

Это меню лучше делать на отдельном Canvas, чтобы можно было сделать его сквозным, т.е. чтобы он переходил из сцены в сцену и не уничтожался. Таким образом, меню можно собрать в стартовой сцене, где и основное меню игры, а далее оно будет переходить на другие сцены. Смысл тут в том, что загрузка ресурсов меню, проходила только в одной сцене, а не каждый раз. Что снизит нагрузку при старте новой сцены.

Теперь на наш Canvas вешаем:

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' - менюшка будет переходить из сцены в сцену

	private static bool _active;
	private static LevelSelect _internal;
	private int groupIndex;
	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)
	{
		int j = 0;

		foreach(LevelData element in data)
		{
			if(!element.isActive) break; else j++;
		}

		if(j <= data.Length-1)
		{
			data[j].isActive = true;
			data[j].score = score;
			data[j].time = time;
		}
		else return;

		StreamWriter writer = new StreamWriter(GetPath());

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

		writer.Close();

		Debug.Log("[LevelSelect] сохранение в файл: " + GetPath());

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

	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 = 0;
		while(!reader.EndOfStream)
		{
			SetLevel(reader.ReadLine(), j);
			j++;
		}

		if(j <= data.Length-1)
		{
			data[j].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();

		SceneManager.LoadScene(level);
	}

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

	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;
		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 + 1));
			}
			else
			{
				element.button.interactable = false;
				element.button.image.sprite = GetSprite(true, scenePrefix + (j + 1));
			}

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

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

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

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

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

Офлайн
Антохич 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
Огромное спасибо за ответ. Я, правда, не программист, совсем. Но попробую разобраться.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика