Search code examples
c#unity-game-enginesliding-tile-puzzle

How should I program my mechanic design for a puzzle game with Unity 2020.3.14f?


I'm coding a puzzle game where you can slide the tiles horizontally or vertically in a 3x3 gridmap.

If you select a tile, when you press up or down arrow key, the column this selected tile is in moves vertically upwards or downwards by 1 unit. Same applies to horizontal movements.

This will cause the blocks to go over the 3x3 boundary. This is why I have another rule that: when a block is over the 3x3 boundary, it is teleported to the vacant position, filling the grid map. So, for example: the horizontal order of blocks could be (1, 2, 3). After sliding this row of blocks to the left by 1 grid, the order becomes (3, 1, 2). Do it again? It is (2, 3, 1). Here's a screenshot of what the level looks like:

enter image description here

I thought it was a really simple logic to code but it has proven me wrong. It is actually fairly tricky.

I initially assigned each block an order number exactly identical to that of the keypad. So, bottom left block would be 1, then 2 on the right, and 3 on the bottom right... Whenever I pressed number key 1 on keypad and pressed up arrow, I hard-coded it and set the vertical order of blocks (1, 4, 7) to (7, 1, 4).

It doesn't work at all because if I don't reset the position back to normal, and start to change another given row or column, the layout of the map becomes messed up. This is because even if I changed the physical position of the blocks, their assigned order is not changed, which means that if the blocks that are going to be moved are not in their normal position, they can overlap onto other blocks.

Anyways, here is an example of the designed mechanic:

I. Normal position:

enter image description here

II. Slided row (1, 2, 3) right by 1 unit

enter image description here

III. Slided column (2, 5, 8) downwards by 1 unit

enter image description here

Can someone please give me some advice? It doesn't have to be in actual code. I just need some directions to go for... I'm out of ideas now.


Solution

  • As pointed out your images are not quite accurate ^^

    Anyway, there might be more efficient and extendable ways but here is what I would do as a first iteration - plain straight forward:

    • Have a grid component which holds a 3x3 grid and handles all the shift operations

      Additionally I will also route all movements through this Grid component in order to easily move tiles together - entire rows or columns - and keep things clean

      I hope the comments are clear enough

       public class Grid : MonoBehaviour
       {
           // To make things simple for the demo I simply have 9 "GridElement" objects and place them later based on their index
           [SerializeField]
           private GridElement[] elements = new GridElement[9];
      
           // stores the current grid state
           private readonly GridElement[,] _grid = new GridElement[3, 3];
      
           private void Awake()
           {
               // go through the grid and assign initial elements to their positions and initialize them
               for (var column = 0; column < 3; column++)
               {
                   for (var row = 0; row < 3; row++)
                   {
                       _grid[column, row] = elements[row * 3 + column];
      
                       _grid[column, row].Initialize(this);
                   }
               }
      
               RefreshIndices();
           }
      
           // Shifts the given column one step up with wrap around
           // => top element becomes new bottom
           public void ShiftColumnUp(int column)
           {
               var temp = _grid[column, 2];
               _grid[column, 2] = _grid[column, 1];
               _grid[column, 1] = _grid[column, 0];
               _grid[column, 0] = temp;
      
               RefreshIndices();
           }
      
           // Shifts the given column one step down with wrap around
           // => bottom element becomes new top
           public void ShiftColumnDown(int column)
           {
               var temp = _grid[column, 0];
               _grid[column, 0] = _grid[column, 1];
               _grid[column, 1] = _grid[column, 2];
               _grid[column, 2] = temp;
      
               RefreshIndices();
           }
      
           // Shifts the given row one step right with wrap around
           // => right element becomes new left
           public void ShiftRowRight(int row)
           {
               var temp = _grid[2, row];
               _grid[2, row] = _grid[1, row];
               _grid[1, row] = _grid[0, row];
               _grid[0, row] = temp;
      
               RefreshIndices();
           }
      
           // Shifts the given row one step left with wrap around
           // => left element becomes new right
           public void ShiftRowLeft(int row)
           {
               var temp = _grid[0, row];
               _grid[0, row] = _grid[1, row];
               _grid[1, row] = _grid[2, row];
               _grid[2, row] = temp;
      
               RefreshIndices();
           }
      
           // Iterates through all grid elements and updates their current row and column indices
           // and applies according positions
           public void RefreshIndices()
           {
               for (var column = 0; column < 3; column++)
               {
                   for (var row = 0; row < 3; row++)
                   {
                       _grid[column, row].UpdateIndices(row, column);
                       _grid[column, row].transform.position = new Vector3(column - 1, 0, row - 1);
                   }
               }
           }
      
           // Called while dragging an element
           // Moves the entire row according to given delta (+/- 1)
           public void MoveRow(int targetRow, float delta)
           {
               for (var column = 0; column < 3; column++)
               {
                   for (var row = 0; row < 3; row++)
                   {
                       _grid[column, row].transform.position = new Vector3(column - 1 + (row == targetRow ? delta : 0), 0, row - 1);
                   }
               }
           }
      
           // Called while dragging an element
           // Moves the entire column according to given delta (+/- 1)
           public void MoveColumn(int targetColumn, float delta)
           {
               for (var column = 0; column < 3; column++)
               {
                   for (var row = 0; row < 3; row++)
                   {
                       _grid[column, row].transform.position = new Vector3(column - 1, 0, row - 1 + (column == targetColumn ? delta : 0));
                   }
               }
           }
       }
      
    • And then accordingly have a GridElement component on each grid element to handle the dragging and route the movement through the Grid

       public class GridElement : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler
       {
           // on indices within the grid so we can forward thm to method calls later
           private int _currentRow;
           private int _currentColumn;
      
           // a mathematical XZ plane we will use for the dragging input
           // you could as well just use physics raycasts 
           // but for now just wanted to keep it simple
           private static Plane _dragPlane = new Plane(Vector3.up, 1f);
      
           // reference to the grid to forward invoke methods
           private Grid _grid;
      
           // the world position where the current draggin was started
           private Vector3 _startDragPoint;
      
           // camera used to convert screenspace mouse position to ray
           [SerializeField]
           private Camera _camera;
      
           public void Initialize(Grid grid)
           {
               // assign the camera 
               if(!_camera)_camera = Camera.main;
      
               // store the grid reference to later forward the input calls
               _grid = grid;
           }
      
           // plain set the indices to the new values
           public void UpdateIndices(int currentRow, int currentColumn)
           {
               _currentRow = currentRow;
               _currentColumn = currentColumn;
           }
      
           // called by the EventSystem when starting to drag this object
           public void OnBeginDrag(PointerEventData eventData)
           {
               // get a ray for the current mouse position
               var ray = _camera.ScreenPointToRay(eventData.position);
      
               // shoot a raycast against the mathemtical XZ plane
               // You could as well use Physics.Raycast and get the exact hit point on the collider etc 
               // but this should be close enough especially in top-down views
               if (_dragPlane.Raycast(ray, out var distance))
               {
                   // store the world space position of the cursor hit point
                   _startDragPoint = ray.GetPoint(distance);
               }
           }
      
           // Called by the EventSystem while dragging this object
           public void OnDrag(PointerEventData eventData)
           {
               var ray = _camera.ScreenPointToRay(eventData.position);
      
               if (_dragPlane.Raycast(ray, out var distance))
               {
                   // get the dragged delta against the start position
                   var currentDragPoint = ray.GetPoint(distance);
                   var delta = currentDragPoint - _startDragPoint;
      
                   // we either only drag vertically or horizontally
                   if (Mathf.Abs(delta.x) > Mathf.Abs(delta.z))
                   {
                       // clamp the delta between -1 and 1
                       delta.x = Mathf.Clamp(delta.x, -1f, 1f);
      
                       // and tell the grid to move this entire row
                       _grid.MoveRow(_currentRow, delta.x);
                   }
                   else
                   {
                       delta.z = Mathf.Clamp(delta.z, -1f, 1f);
      
                       // accordingly tell the grid to move this entire column
                       _grid.MoveColumn(_currentColumn,delta.z);
                   }
               }
           }
      
           // Called by the EventSystem when stop dragging this object
           public void OnEndDrag(PointerEventData eventData)
           {
               var ray = _camera.ScreenPointToRay(eventData.position);
      
               if (_dragPlane.Raycast(ray, out var distance))
               {
                   // as before get the final delta
                   var currentDragPoint = ray.GetPoint(distance);
                   var delta = currentDragPoint - _startDragPoint;
      
                   // Check against a threashold - if simply went with more then the half of one step
                   // and shift the grid into the according direction
                   if (delta.x > 0.5f)
                   {
                       _grid.ShiftRowRight(_currentRow);
                   }else if (delta.x < -0.5f)
                   {
                       _grid.ShiftRowLeft(_currentRow);
                   }
                   else if (delta.z > 0.5f)
                   {
                       _grid.ShiftColumnUp(_currentColumn);
                   }
                   else if(delta.z < -0.5f)
                   {
                       _grid.ShiftColumnDown(_currentColumn);
                   }
                   else
                   {
                       // if no direction matched at all just make sure to reset the positions
                       _grid.RefreshIndices();
                   }
               }
           }
       }
      
    • Finally in order to make this setup work you will need

      • an EventSystem component anywhere in your scene
      • a PhysicsRaycaster component on your Camera

    Little demo

    enter image description here