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

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


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

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

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

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

Офлайн
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];
}
Офлайн
labrico 8 марта 2018
А как сделать, перемещение не мгновенным а плавным?
Офлайн
Light 8 марта 2018
labrico, в двух словах не опишешь, надо скрипты переделывать.
Офлайн
andrey060100 12 июля 2018
А можно ли уменьшить расстояние между картинками?
Офлайн
Light 13 июля 2018
andrey060100, в скрипте есть комментарий "отступ по Х и Y".
Офлайн
Katerina1993 17 сентября 2018
Как вызвать ReplaceBlocks(int x, int y, int XX, int YY)

Всё понятно с помощью onmousedown()

Вообщем код не работает выдаёт ошибку IndexOutOfRangeException: Array index is out of range. У меня нету кнопки GameObject->UI->Button так как Unity 4 (все мне говорят обнови до Unity 5, нет возможности). По этому я реализовала вот так:

void OnGUI()
	{
		if (GUI.Button (new Rect (10, Screen.height / 2 - 100, 150, 25), "New Game")) 
		{
			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;
			}
			StartNewGame();
		}
	}

В чём может быть проблема?
Офлайн
Katerina1993 17 сентября 2018
Оказывается в скрипте GameControl, нужно вручную заполнить массивы своими префабами. При нажатии новая игра создаётся пятнадцать элементов, но кнопка void onmousedown() не работает не на одном элементе.
Офлайн
Katerina1993 17 сентября 2018
Всё готово сделала.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика