Monthly Archives: March 2016

Word Game Drag-and-Snap

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:

drop-snap

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.

Drawing Lines with LineRenderer

Unity’s LineRenderer component gives you the ability to draw lines with multiple segments. You can use this to draw anything line based, like a rope or a finger drawing.

This tutorial is for intermediate users: I will expect you to know your way around Unity reasonably well and understand basic code without explanation. Please see my earlier tutorials if you’re a beginner.

What is a LineRenderer?

A LineRenderer component is a standard Unity component that draws a line (in one or more segments) based on the settings you provide. You can set several options, most importantly:

  • The line’s positions (each point along the line, which will be connected to each other to form the line)
  • The line’s starting and ending thickness (how wide the line is drawn at its beginning and at its end)
  • The material that determines how the line looks.

Adding a LineRenderer

To use a LineRenderer you can add one in the Inspector (confusingly, it’s under Effects, not Rendering), or you can add one in code:

var line = gameObject.AddComponent<LineRenderer>();

Here’s a screenshot of a simple LineRenderer with some sample settings you can copy into your own project:

I’ve manually filled in the positions and line thickness, but you can of course do this in code too.

You can only have a single LineRenderer component attached to a transform. If you need several, you can create a child transform for each on the parent.

Coding a LineRenderer

To setup your line you’ll need to give it the appropriate settings. Here’s a basic method to setup a LineRenderer with the key properties:

void SetupLine()
{
line.sortingLayerName = "OnTop";
line.sortingOrder = 5;
line.SetVertexCount(2);
line.SetPosition(0, startingPoint);
line.SetPosition(1, middlePoint);
line.SetPosition(2, endPoint);
line.SetWidth(0.5f, 0.5f);
line.useWorldSpace = true;
line.material = LineMaterial;
}

Let’s go through that code line by line.

line.sortingLayerName = "OnTop";
line.sortingOrder = 5;

These lines sets the line’s sorting layer and order. In my testing I’ve found that you need to set this when the line is created, as changing it later doesn’t have any effect. Make sure you set the line how you need it in relation to other objects in your scene.

line.SetVertexCount(3);
line.SetPosition(0, startingPoint);
line.SetPosition(1, middlePoint);
line.SetPosition(2, endPoint);

These lines set how many points make up the line, and then adds a position to each point forming the line’s shape. Here I’ve set a line to have three points, then set each to the value of a vector variable.

Note that the vertex count is 1-based, but SetPosition is 0-based.
line.SetWidth(0.5f, 0.5f);

This line sets the line’s width at the start and end (i.e. at the first and last points on the line). If you set both to be the same, the line will have a uniform thickness, otherwise the line will taper towards the end that is thinnest.

line.useWorldSpace = true;

useWorldSpace determines if the positions you set are based on ‘world space’ or are relative to the line’s transform’s position. You’ll usually want to set this as true.

 line.material = LineMaterial;

This line sets the material used to draw the line. If you don’t set a material the line will be magenta by default.

LineRenderer Weirdness

No GetVertexCount or GetPosition

For some reason there is apparently no way to get the number of positions in a LineRenderer or to add a new position without keeping track of the line details yourself. So I always keep a List<Vector2> (for 2D lines) and update that list every time I need to change the line’s positions.

Here’s a method to add a new point to extend an existing line:

void AddLinePoint(Vector2 newPoint)
{
       storedLinePoints.Add(newPoint); // add the new point to our saved list of line points
       line.SetVertexCount(storedLinePoints.Count); // set the line’s vertex count to how many points we now have, which will be 1 more than it is currently
       line.SetPosition(storedPoints.Count-1, newPoint); // add newPoint as the last point on the line (count -1 because the SetPosition is 0-based and Count is 1-based)    
}

As you can see, we need to keep our own list of points in order to know how many points there are so we can add a new one. You can similarly remove a point from the line:

void RemoveLastLinePoint()
{
       storedLinePoints.RemoveAll(storedLinePoints.Count - 1); // remove the last point from the line
       line.SetVertexCount(storedLinePoints.Count); // set the line’s vertex count to how many points we now have, which will be 1 fewer than it is currently       
}

That method removes the most recently added point from the line.

Weird Tapering

Unfortunately, LineRenderers have weird behaviour when they are made to turn on sharp angles. Due to the way they are rendered you will get the appearance of tapered line segments even when you have set the thickness to be even. There is no way around this; it’s just how it works. You won’t see the problem if you have thin lines or avoid sharp angles. You can mitigate the problem by adding some extra points to your line, especially near the sharp turns.

 

That’s It

That’s the basics of Unity’s LineRenderer! Experiment with different settings to get different line looks.