15 Puzzle / «Пятнашки» на Unity

На этот раз будем клонировать «Пятнашки» (The 15-puzzle), популярная игра, придуманная в далеком 1878 году. Невероятная графика и завораживающая физика, увы, не в этот раз)) Впрочем, при желании, красивую графику сделать можно. Цель игры в принципе проста, есть шестнадцать клеток, пятнадцать из которых заняты пронумерованными «костяшками», которые в свою очередь перемешаны в случайном порядке. Надо по номерам расставить по нарастанию, начиная с верхнего левого угла, а пустая клетка должна оказаться в нижнем правом углу. Вот такую головоломку мы и будем делать. Плюс добавим, автопроверку на выигрыш и кнопку с возможностью начать новую игру.


Создаем новый 2D проект.
Особых настроек сцены нет, возможна корректировка камеры, в зависимости от размера используемых пронумерованных изображений. Кстати говоря, их надо сделать заранее, пятнадцать картинок с соответствующими номерами. На каждый спрайт повесть 2D коллайдер, и перетаскиваем в папку Prefab. Эти пятнадцать префабов обновим чуть позже, а сейчас, добавим на сцену пустой объект и вешаем на него главный скрипт игры GameControl:

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

public class GameControl : MonoBehaviour {

	public GameObject[] _puzzle; // оригинальный массив

	// стартовая позиция для первого элемента
	public float startPosX = -6f;
	public float startPosY = 6f;

	// отступ по Х и Y, рассчитывается в зависимости от размера объекта
	public float outX = 1.1f;
	public float outY = 1.1f;

	public Text _text; // вывод текстовой информации

	public static int click;
	public static GameObject[,] grid;
	public static Vector3[,] position;
	private GameObject[] puzzleRandom;
	public static bool win;
	
	void Start () 
	{
		puzzleRandom = new GameObject[_puzzle.Length];

		// заполнение массива позиций клеток
		float posXreset = startPosX;
		position = new Vector3[4,4];
		for(int y = 0; y < 4; y++)
		{
			startPosY -= outY;
			for(int x = 0; x < 4; x++)
			{
				startPosX += outX;
				position[x,y] = new Vector3(startPosX, startPosY, 0);
			}
			startPosX = posXreset;
		}

		if(!PlayerPrefs.HasKey("Puzzle")) StartNewGame(); else Load();
	}

	public void StartNewGame()
	{
		win = false;
		click = 0;
		RandomPuzzle();
		Debug.Log("New Game");
	}

	public void ExitGame()
	{
		Save();
		Application.Quit();
	}

	void Save()
	{
		string content = string.Empty;
		for(int y = 0; y < 4; y++)
		{
			for(int x = 0; x < 4; x++)
			{
				if(content.Length > 0) content += "|";
				if(grid[x,y]) content += grid[x,y].GetComponent<Puzzle>().ID.ToString(); else content += "null";
			}
		}
		PlayerPrefs.SetString("Puzzle", content);
		PlayerPrefs.SetString("PuzzleInfo", click.ToString());
		//PlayerPrefs.Save(); // записать на диск сейчас, если в приложении не используется функция выхода
		Debug.Log(this + " сохранение 15 Puzzle.");
	}

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

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

		if(PlayerPrefs.HasKey("PuzzleInfo")) click = Parse(PlayerPrefs.GetString("PuzzleInfo"));

		grid = new GameObject[4,4];
		int i = 0;
		for(int y = 0; y < 4; y++)
		{
			for(int x = 0; x < 4; x++)
			{
				int j = FindPuzzle(Parse(content[i]));

				if(j >= 0)
				{
					grid[x,y] = Instantiate(_puzzle[j], position[x,y], Quaternion.identity) as GameObject;
					grid[x,y].name = "ID-"+i;
					grid[x,y].transform.parent = transform;

				}
				i++;
			}
		}
	}

	int FindPuzzle(int index)
	{
		int j = 0;
		foreach(GameObject e in _puzzle)
		{
			if(e.GetComponent<Puzzle>().ID == index) return j;
			j++;
		}
		return -1;
	}

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

	void CreatePuzzle()
	{
		if(transform.childCount > 0)
		{
			// удаление старых объектов, если они есть
			for(int j = 0; j < transform.childCount; j++)
			{
				Destroy(transform.GetChild(j).gameObject);
			}
		}
		int i = 0;
		grid = new GameObject[4,4];
		int h = Random.Range(0,3);
		int v = Random.Range(0,3);
		GameObject clone = new GameObject();
		grid[h,v] = clone; // размещаем пустой объект в случайную клетку
		for(int y = 0; y < 4; y++)
		{
			for(int x = 0; x < 4; x++)
			{
				// создание дубликатов на основе временного массива
				if(grid[x,y] == null)
				{
					grid[x,y] = Instantiate(puzzleRandom[i], position[x,y], Quaternion.identity) as GameObject;
					grid[x,y].name = "ID-"+i;
					grid[x,y].transform.parent = transform;
					i++;
				}
			}
		}
		Destroy(clone); 
		for(int q = 0; q < _puzzle.Length; q++)
		{
			Destroy(puzzleRandom[q]);
		}
	}

	static public void GameFinish()
	{
		int i = 1;
		for(int y = 0; y < 4; y++)
		{
			for(int x = 0; x < 4; x++)
			{
				if(grid[x,y]) { if(grid[x,y].GetComponent<Puzzle>().ID == i) i++; } else i--;
			}
		}
		if(i == 15)
		{
			for(int y = 0; y < 4; y++)
			{
				for(int x = 0; x < 4; x++)
				{
					if(grid[x,y]) Destroy(grid[x,y].GetComponent<Puzzle>());
				}
			}
			win = true;
			Debug.Log("Finish!");
		}
	}

	// создание временного массива, с случайно перемешанными элементами
	void RandomPuzzle()
	{
		int[] tmp = new int[_puzzle.Length];
		for(int i = 0; i < _puzzle.Length; i++)
		{
			tmp[i] = 1;
		}
		int c = 0;
		while(c < _puzzle.Length)
		{
			int r = Random.Range(0, _puzzle.Length);
			if(tmp[r] == 1)
			{ 
				puzzleRandom[c] = Instantiate(_puzzle[r], new Vector3(0, 10, 0), Quaternion.identity) as GameObject;
				tmp[r] = 0;
				c++;
			}
		}
		CreatePuzzle();
	}

	void LateUpdate () 
	{
		if(!win)
		{
			_text.text = "Ходов:\n" + click;
		}
		else
		{
			click = 0;
			_text.text = "Игра\nЗавершена!";
		}
	}
}

Теперь в массив _puzzle сразу добавим ранее сделанные префабы.

Создадим еще один скрипт Puzzle:

using UnityEngine;
using System.Collections;

public class Puzzle : MonoBehaviour {

	public int ID; // номер должен соответствовать данной "костяшки"

	// текущая и пустая клетка, меняются местами
	void ReplaceBlocks(int x, int y, int XX, int YY)
	{
		GameControl.grid[x,y].transform.position = GameControl.position[XX,YY];
		GameControl.grid[XX,YY] = GameControl.grid[x,y];
		GameControl.grid[x,y] = null;
		GameControl.click++;
		GameControl.GameFinish();
	}

	void OnMouseDown()
	{
		for(int y = 0; y < 4; y++)
		{
			for(int x = 0; x < 4; x++)
			{
				if(GameControl.grid[x,y])
				{
					if(GameControl.grid[x,y].GetComponent<Puzzle>().ID == ID)
					{
						if(x > 0 && GameControl.grid[x-1,y] == null)
						{
							ReplaceBlocks(x,y,x-1,y);
							return;
						}
						else if(x < 3 && GameControl.grid[x+1,y] == null)
						{
							ReplaceBlocks(x,y,x+1,y);
							return;
						}
					}
				}
				if(GameControl.grid[x,y])
				{
					if(GameControl.grid[x,y].GetComponent<Puzzle>().ID == ID)
					{
						if(y > 0 && GameControl.grid[x,y-1] == null)
						{
							ReplaceBlocks(x,y,x,y-1);
							return;
						}
						else if(y < 3 && GameControl.grid[x,y+1] == null)
						{
							ReplaceBlocks(x,y,x,y+1);
							return;
						}
					}
				}
			}
		}
	}
}

Его надо повесить на префабы. Внимание! Не забудьте сразу указать ID, соответствующий номеру картинки.

Почти готово, осталось пару штрихов. Добавим на сцену несколько объектов UI, для вывода текста (GameObject->UI->Text), его надо кстати указать для первого скрипта. И еще нам понадобится пара кнопок (GameObject->UI->Button), одна для перезапуска игры, а вторая для выхода из игры. Чтобы назначить кнопке действие, нужно добавить ей элемент, нажав на плюс, а затем перетащить в этот элемент объект со сцены, где находится нужный скрипт:

15 Puzzle / «Пятнашки» на Unity

И из выдающего меню выбрать нужную функцию:

15 Puzzle / «Пятнашки» на Unity

Для кнопки "Новая игра", нужно выбрать функцию StartNewGame, для кнопки "Выход" функция ExitGame. Собственно, на этом всё.)

Дополнительно, скачать Проект «Пятнашки»:

У вас нет доступа!

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

Офлайн
Kykyda 25 ноября 2015
в коде непростительная ошибка, повергшая меня в тупик на долгие часы
класс Puzzle
"
void onmousedown()
{
"

Хотел написать исправление, но сайт автоматом переводит в нижний регистр. В общем в названии метода каждое слово с большой буквы
Офлайн
Light 26 ноября 2015
Kykyda,
Действительно, странный глюк DLE.
Надо: On Mouse Down слитно.
Офлайн
Challenger 7 мая 2016
подскажите как изменится код если будет использоваться не спрайты а трехмерные кубы?
Офлайн
Light 7 мая 2016
Challenger, да по сути никак, если не нужно двигать по Z.
Офлайн
redguru 8 августа 2016
Здравствуйте, коллеги кто знает можно вместо изображений кнопки с текстом подгрузить. Спасибо.
Офлайн
Light 8 августа 2016
redguru, всмысле под UI? В текущем виде, код не подходит для этой системы, нужно переделывать.
Офлайн
redguru 9 августа 2016
Light,
Понятно. Я то думал что Unity это что хочешь то и делаешь а он какой то узковатый. Спасибо.
Офлайн
Light 9 августа 2016
Цитата: redguru
Я то думал что Unity это что хочешь то и делаешь а он какой то узковатый

В любом деле, мало хотеть, нужно еще и уметь. Unity предоставляет возможности, а как ты их используешь, зависит от личных знаний и опыта.
Офлайн
redguru 9 августа 2016
Light,
Да это понятно я не хотел обидеть инструмент извените. Но например на WPF можно с объектом делать что хочешь но он мне для Android и iOS не подходит. Хотел поюзать Unity так как есть идея монетизации но видать придётся пилить вручную. Спасибо большое.
Офлайн
Добрый день. Разбирал демку этой игрушки и хотел спросить. Баловался с размерами окна и встретился с такой проблемой, что кнопки и текст наезжают на игровое поле. И хотел спросить, каким образом осуществлять сохранение, т.е. ID кнопки + ее x и y позиции, а потом загрузку?
Офлайн
Light 1 сентября 2016
Цитата: Женя Мартынив
что кнопки и текст наезжают на игровое поле

Настроить UI якоря.

Цитата: Женя Мартынив
т.е. ID кнопки + ее x и y позиции, а потом загрузку

Да, сохранить айдишники и текущие позиции на поле.
Офлайн
Light,
Спасибо
Офлайн
И снова здравствуйте, так и не смог нормально сделать сохранение для данной игры. Скажите пожалуйста, есть ли уроки, где очень хорошие наглядные примеры, прямо, вот чтобы почти такой же случай.
Офлайн
Light 3 сентября 2016
Женя Мартынив, там суть в том, чтобы пройтись по сетке (двумерный массив) и сохранить айдишники. А при загрузке, по порядку восстановить как было, т.е. в каждую клетку запихать префаб с соответствующим айди.

- проект обновлен, добавлена возможность сохранения/загрузки.

Сохранение происходит в момент нажатия кнопки "Выход".

Для тех кто ранее скачал:
Создайте новый проект. И импортируйте обновленный unitypackage.
Офлайн
Light,
Спасибо большое.
Офлайн
vadjuk 14 октября 2016
Вечер добрый! Как проще всего было бы реализовать загрузку изображения на выбор в пятнашках во время выполнения? Есть какие-нибудь варианты, без перестраивания логики? Может быть использования масок на префабы или что-то другое?
Офлайн
Light 17 октября 2016
vadjuk, нужно создать публичный массив с новыми спрайтами.

Например:

public Sprite[] mySprites;

И запихать туда спрайты от номера 1 и до 15.

Затем можно делать замену через простую функцию:

for(int i = 0; i < _puzzle; i++)
{
	_puzzle[i].GetComponent<SpriteRenderer>().sprite = mySprites[i];
}
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Дешевый хостинг
  • Яндекс.Метрика