Intermediate: I expect you to know a moderate amount of Unity and C# coding to follow this tutorial, and I don’t go into beginner-level detail. Please refer to my Beginner’s Pong Tutorial if you’re a beginner and find this tutorial too advanced. You can still download the full demo project below and poke around.

Following on from my previous article explaining drag-and-drop functionality, I’d like to show you how to implement ‘snapping’ objects to a specific place, such as a Scrabble grid:

How does it work?

We will create a simple Scrabble/Words with Friends demo, with a few tiles that can be dragged onto a board grid, so we’ll:

Create a board with a grid of possible letter tile positions.

Create tiles with different letters on.

Allow dragging of the tile objects.

Determine where a tile should be placed when it is dropped.

Smoothly move the tile to the correct position for a nice feel.

Get started

Create a new Unity project and choose the 2D settings. Download the sprites package below.

I’ve included some public domain letter tiles in the project. As usual I use art by Kenney, who provides tons of great free art for the game development community. You can find his art – and ways to support him – at Kenney’s website

Build a game board

To build a game board, I created a simple square cell prefab with a sprite and a BoxCollider2D. I then created a row from 5 cells side by side. Finally, I stacked 5 rows vertically to create a grid. There’s nothing notable about the grid, so you should be able to create your own without detailed instructions, or you can just copy the one in the sample project.

I gave the grid its own layer so that I could tell the InputManager to ignore it. This ensures that the input doesn’t incorrectly detect touches on the grid when the player is trying to grab a letter, and raycasts can be a little unusual if you’re not careful.

Drag and Drop Script

The script that controls the drag-and-drop functionality is basically the same script from my previous article, so I won’t explain it again here. The main difference here is that the script calls some public methods on the Tile objects when a tile is picked up or dropped.

Here’s the drag and drop script in full.

Note: This script calls methods on the Tile script, so you can’t test this script out until you’ve also created the Tile script, which we’ll do next.

using UnityEngine; using System.Collections; public class InputManager : MonoBehaviour { private bool draggingItem = false; private GameObject draggedObject; private Vector2 touchOffset; void Update() { if (HasInput) { DragOrPickUp(); } else { if (draggingItem) DropItem(); } } Vector2 CurrentTouchPosition { get { return Camera.main.ScreenToWorldPoint(Input.mousePosition); } } private void DragOrPickUp() { var inputPosition = CurrentTouchPosition; if (draggingItem) { draggedObject.transform.position = inputPosition + touchOffset; } else { RaycastHit2D[] touches = Physics2D.RaycastAll(inputPosition, inputPosition, 0.5f); if (touches.Length > 0) { var hit = touches[0]; if (hit.transform != null && hit.transform.tag == "Tile") { draggingItem = true; draggedObject = hit.transform.gameObject; touchOffset = (Vector2)hit.transform.position - inputPosition; hit.transform.GetComponent<Tile>().PickUp(); } } } } private bool HasInput { get { // returns true if either the mouse button is down or at least one touch is felt on the screen return Input.GetMouseButton(0); } } void DropItem() { draggingItem = false; draggedObject.transform.localScale = new Vector3(1, 1, 1); draggedObject.GetComponent<Tile>().Drop(); } }

The Tile Script

The tiles themselves have a script that handles what happens when the tile is manipulated.

Before we get to the full Tile script code I’ll explain what the script does.

We have two public methods (that are called by the InputManager script) to handle picking up and dropping the tile.

PickUp()

This method simply makes the tile a big larger and raises its sprite’s sorting order so it is always above whatever you’re dragging it over.

transform.localScale = new Vector3(1.1f,1.1f,1.1f); gameObject.GetComponent<SpriteRenderer>().sortingOrder = 1;

Drop()

The important code in the Tile script is the Drop method. I’ll explain each piece of the method separately, and you can see it all together in the full Tile.cs script later.

The first thing we do is undo the scaling and sorting adjustment that was done when the tile was picked up:

transform.localScale = new Vector3(1, 1, 1); gameObject.GetComponent<SpriteRenderer>().sortingOrder = 0;

In the OnTriggerEnter2D() and OnTriggerExit2D() methods, we keep a running list of any grid cells that the tile is currently touching. The code for that is straightforward, and you can see it in the full script code, so I won’t detail it here.

Knowing that we are keeping track of grid cells in contact with the tile we first check if the tile it touching any grid cells. If not, we set the tile back to its original starting position and make sure its parent is reset (as you’ll see in a moment, when a tile is dropped on the grid we make it a child of whichever cell it was placed into).

if (touchingTiles.Count == 0) { transform.position = startingPosition; transform.parent = myParent; return; }

Deciding the Closest Cell

If the tile is touching only 1 cell, we drop the tile into that cell; if multiple cells are being touched, we cycle through them all and figure out which is the closest.

var currentCell = touchingTiles[0]; if (touchingTiles.Count == 1) { newPosition = currentCell.position; } else { var distance = Vector2.Distance(transform.position, touchingTiles[0].position); foreach (Transform cell in touchingTiles) { if (Vector2.Distance(transform.position, cell.position) < distance) { currentCell = cell; distance = Vector2.Distance(transform.position, cell.position); } } newPosition = currentCell.position; }

Finally, we have to make sure the cell is not occupied before dropping the tile into it:

if (currentCell.childCount != 0) { transform.position = startingPosition; transform.parent = myParent; return; } else { transform.parent = currentCell; StartCoroutine(SlotIntoPlace(transform.position, newPosition)); }

Smoothing the Drop

One last thing we do is to make the tile slide neatly into its cell so it doesn’t appear to suddenly snap into place.

We achieve a smooth effect by using a co-routine to gradually move the tile’s position over a period of time until it’s exactly centered on the cell. We use the tile’s current position as the starting point and the cell’s position as the destination. Then we use Vector2.Lerp to move the tile closer and closer to the destination over the amount of time we specify.

One extra trick I added was to force the tile’s position to match the end position exactly once the co-routine has run its course. This prevents the possibility of the tile not perfectly lining up with the cell’s position due to rounding errors (you can think of it as a final check: if the tile’s position is not the same as the cell’s position then force it).

IEnumerator SlotIntoPlace(Vector2 startingPos, Vector2 endingPos) { float duration = 0.1f; float elapsedTime = 0; while (elapsedTime < duration) { transform.position = Vector2.Lerp(startingPos, endingPos, elapsedTime / duration); elapsedTime += Time.deltaTime; yield return new WaitForEndOfFrame(); } transform.position = endingPos; }

The Full Tile Script

Here’s the tile script in full. I’ve also added an audio source to the tiles in the demo so the tiles make a nice sound when they slide into place.

using UnityEngine; using System.Collections; using System.Collections.Generic; public class Tile : MonoBehaviour { private Vector2 startingPosition; private List<Transform> touchingTiles; private Transform myParent; private AudioSource audSource; private void Awake() { startingPosition = transform.position; touchingTiles = new List<Transform>(); myParent = transform.parent; audSource = gameObject.GetComponent<AudioSource>(); } public void PickUp() { transform.localScale = new Vector3(1.1f,1.1f,1.1f); gameObject.GetComponent<SpriteRenderer>().sortingOrder = 1; } public void Drop() { transform.localScale = new Vector3(1, 1, 1); gameObject.GetComponent<SpriteRenderer>().sortingOrder = 0; Vector2 newPosition; if (touchingTiles.Count == 0) { transform.position = startingPosition; transform.parent = myParent; return; } var currentCell = touchingTiles[0]; if (touchingTiles.Count == 1) { newPosition = currentCell.position; } else { var distance = Vector2.Distance(transform.position, touchingTiles[0].position); foreach (Transform cell in touchingTiles) { if (Vector2.Distance(transform.position, cell.position) < distance) { currentCell = cell; distance = Vector2.Distance(transform.position, cell.position); } } newPosition = currentCell.position; } if (currentCell.childCount != 0) { transform.position = startingPosition; transform.parent = myParent; return; } else { transform.parent = currentCell; StartCoroutine(SlotIntoPlace(transform.position, newPosition)); } } void OnTriggerEnter2D(Collider2D other) { if (other.tag != "Cell") return; if (!touchingTiles.Contains(other.transform)) { touchingTiles.Add(other.transform); } } void OnTriggerExit2D(Collider2D other) { if (other.tag != "Cell") return; if (touchingTiles.Contains(other.transform)) { touchingTiles.Remove(other.transform); } } IEnumerator SlotIntoPlace(Vector2 startingPos, Vector2 endingPos) { float duration = 0.1f; float elapsedTime = 0; audSource.Play(); while (elapsedTime < duration) { transform.position = Vector2.Lerp(startingPos, endingPos, elapsedTime / duration); elapsedTime += Time.deltaTime; yield return new WaitForEndOfFrame(); } transform.position = endingPos; } }

That’s All, Folks

That’s everything. You can check the demo project if you can’t recreate the functionality fully.