I've made the Game of Fifteen in Unity this is how it looks :
The code is rather short there's one class for the pieces and one for the game logic :
MovablePiece : MonoBehaviour
public class MovablePiece : MonoBehaviour
{
public PuzzleGrid.SlotPosition CurrentSlotPosition { get; set; }
private void Start()
{
GetComponent<Button>().onClick.AddListener(MoveOnClick);
}
private void MoveOnClick()
{
if (CurrentSlotPosition.Y == PuzzleGrid.CurrentEmptySlotPosition.Y &&
(CurrentSlotPosition.X + 1 == PuzzleGrid.CurrentEmptySlotPosition.X ||
CurrentSlotPosition.X - 1 == PuzzleGrid.CurrentEmptySlotPosition.X))
{
StartCoroutine(AwaitMovement());
}
if (CurrentSlotPosition.X == PuzzleGrid.CurrentEmptySlotPosition.X &&
(CurrentSlotPosition.Y + 1 == PuzzleGrid.CurrentEmptySlotPosition.Y ||
CurrentSlotPosition.Y - 1 == PuzzleGrid.CurrentEmptySlotPosition.Y))
{
StartCoroutine(AwaitMovement());
}
}
private IEnumerator AwaitMovement()
{
Vector3 currentPosition = transform.position;
while (transform.position.normalized != PuzzleGrid.EmptySlot.transform.position.normalized)
{
transform.position = Vector3.MoveTowards(transform.position, PuzzleGrid.EmptySlot.transform.position, 10*Time.deltaTime);
yield return null;
}
GameObject temp = PuzzleGrid.grid[PuzzleGrid.CurrentEmptySlotPosition.X][PuzzleGrid.CurrentEmptySlotPosition.Y];
PuzzleGrid.grid[PuzzleGrid.CurrentEmptySlotPosition.X][PuzzleGrid.CurrentEmptySlotPosition.Y] = gameObject;
PuzzleGrid.grid[CurrentSlotPosition.X][CurrentSlotPosition.Y] = temp;
PuzzleGrid.EmptySlot.transform.position = currentPosition;
PuzzleGrid.SlotPosition tempPosition = PuzzleGrid.CurrentEmptySlotPosition;
PuzzleGrid.CurrentEmptySlotPosition = CurrentSlotPosition;
CurrentSlotPosition = tempPosition;
}
}
The main function in the class is the courotine AwaitMovement
which is called in the button click eventhandler, I figured that for a block to be considered movable it needs to be either on the same Y axis, or on the same X axis with a difference in the other of 1. Here's an illustration where 1,1 is the empty block, the rest are the blocks that can be currently moved.
This is what we are checking in the button click event. Inside the actual courotine we are updating the position of the empty space
PuzzleGrid : MonoBehaviour
public class PuzzleGrid : MonoBehaviour
{
private const int GridSize = 16;
public static List<GameObject>[] grid = new List<GameObject>[4];
private static Text GameStateText;
private static Text MovesText;
private static int movesCount;
private static SlotPosition previousSlotPosition;
private static SlotPosition currentEmptySlotPosition;
public static SlotPosition CurrentEmptySlotPosition
{
get { return currentEmptySlotPosition; }
set
{
if (value.X == previousSlotPosition.X && value.Y == previousSlotPosition.Y)
{
movesCount--;
currentEmptySlotPosition = default(SlotPosition);
}
else
{
movesCount++;
}
previousSlotPosition = currentEmptySlotPosition;
MovesText.text = "Moves : " + movesCount;
currentEmptySlotPosition = value;
if (CheckWin())
{
GameStateText.text = @"You Won !";
}
}
}
public static GameObject EmptySlot { get; set; }
private readonly System.Random rnd = new System.Random();
private static readonly int[] orderNumbers = new int[GridSize - 1];
private void Awake()
{
GetComponent<GridLayoutGroup>().enabled = true;
movesCount = 0;
Image[] imageObjects = FindObjectsOfType<Image>();
GameStateText =
imageObjects
.Where(t => t.name == "GameStateText")
.Select(t => t.GetComponentInChildren<Text>())
.First();
MovesText =
imageObjects
.Where(t => t.name == "MovesText")
.Select(t => t.GetComponentInChildren<Text>())
.First();
}
private void Start()
{
for (int i = 0; i < orderNumbers.Length; i++)
{
orderNumbers[i] = i + 1;
}
SetupGrid();
SetupPieces();
}
private void SetupGrid()
{
GameObject[] pieces =
GetComponentsInChildren<Image>().Where(c => c.tag == "Piece").Select(c => c.gameObject).ToArray();
int index = 0;
for (int i = 0; i < grid.Length; i++)
{
grid[i] = new List<GameObject>();
for (int j = 0; j < grid.Length; j++)
{
grid[i].Add(pieces[index]);
index++;
}
}
GetComponent<GridLayoutGroup>().enabled = false;
}
private void SetupPieces()
{
List<int> numbers = new List<int>(GridSize);
for (int i = 0; i < GridSize; i++)
{
numbers.Add(i + 1);
}
numbers = Shuffle(numbers).ToList();
int indexToRemove = numbers.IndexOf(GridSize);
numbers.RemoveAt(indexToRemove);
currentEmptySlotPosition = new SlotPosition(indexToRemove/grid.Length, indexToRemove%grid.Length);
EmptySlot = grid[CurrentEmptySlotPosition.X][CurrentEmptySlotPosition.Y];
EmptySlot.SetActive(false);
int index = 0;
for (int i = 0; i < grid.Length; i++)
{
for (int j = 0; j < grid[i].Count; j++)
{
if (i != CurrentEmptySlotPosition.X || j != CurrentEmptySlotPosition.Y)
{
grid[i][j].GetComponentInChildren<Text>().text = numbers[index].ToString();
grid[i][j].GetComponent<MovablePiece>().CurrentSlotPosition = new SlotPosition(i, j);
index++;
}
}
}
}
public void StartNewGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
private IList<T> Shuffle<T>(IList<T> array)
{
int n = array.Count;
for (int i = 0; i < n; i++)
{
int r = i + (int) (rnd.NextDouble()*(n - i));
T t = array[r];
array[r] = array[i];
array[i] = t;
}
return array;
}
private static bool CheckWin()
{
int index = 0;
for (int i = 0; i < grid.Length; i++)
{
for (int j = 0; j < grid.Length; j++)
{
if (orderNumbers[index] != int.Parse(grid[i][j].GetComponentInChildren<Text>().text))
{
return false;
}
index++;
if (index >= orderNumbers.Length)
{
return true;
}
}
}
return true;
}
public struct SlotPosition
{
public int X { get; set; }
public int Y { get; set; }
public SlotPosition(int x, int y) : this()
{
X = x;
Y = y;
}
}
}
My idea here was to create the board and the piece before the game is started because it's easier to sort them and arrange them in the order I want. I'm using a GridLayoutGroup to sort the buttons and once the game is started I'm removing it so that, whenever we pick the empty slot button and we disable it, it wont automatically rearrange to the last position (bottom right corner).
I was initially removing the empty slot button completely by destroying it which made my grid uneven on columns so I went for a jagged array but later, I figured I can just disable it and it will still be fine in which case a normal 2D array would still do but I decided to leave it as it is.
AwaitMovement
method complete? It returns anIEnumerator
but there is only one return and it's yielding null - or I don't know how it works. Could you post theStartCoroutine
too? \$\endgroup\$