Master Drag & Drop in Unity: Build Engaging Puzzle Games (Step-by-Step Tutorial)
Hey there, fellow game creators! ๐
Ever played those super addictive “Brain Test” games where you drag objects around, tap on hidden elements, and just know you're smarter for solving them? Well, guess what? You're about to learn the magic behind those engaging interactions in Unity!
This isn't just about making things move — it's about building a solid, scalable system for interactive puzzles. We're going to dive deep into implementing drag-and-drop, making our objects smart, and setting up a flexible puzzle system using Unity's awesome Scriptable Objects.
Ready to make your game objects come alive? Let's get started! ๐ฎ
๐งฉ Our Grand Plan: What We’re Building
Before we jump into the code, let’s get a clear picture of our goals for this session:
-
Make Objects Drag-Friendly: We’ll tweak our existing
TapInteractablescript to allow objects to be dragged around the screen. -
Smart Drop Targets: Our game needs to know when you drop an object exactly where it belongs.
-
Puzzle Master (GameManager): Our central
GameManagerwill handle drag-and-drop logic and keep track of puzzle progress. -
Dynamic Puzzles: We’ll introduce a way to define many puzzles without writing tons of code for each one — using Scriptable Objects.
๐ก Sounds like a lot? Don’t worry — we’ll go step-by-step with clear explanations and visuals. You’ve got this!
Step 1: ๐ง The InteractableObject – More Than Just a Tapper!
First things first, our old TapInteractable script is getting an upgrade. It’s going to handle both taps and drags, so it deserves a shiny new name!
1.1 Rename Your Script in Unity
A quick but important cleanup step to stay organized:
-
In your Unity Project window, open your
_Scriptsfolder. -
Find
TapInteractable.cs, right-click it, and select Rename. -
Rename it to
InteractableObject.csand press Enter. -
If Unity asks to reload the project, click Reload.
✅ Unity automatically updates the class name inside the script — convenient, right?
1.2 Open and Supercharge InteractableObject.cs
Now, let’s open the renamed script in your favorite code editor (VS Code, Rider, or Visual Studio).
We’ll tell Unity that this object can be dragged by implementing the interfaces:
IBeginDragHandler, IDragHandler, and IEndDragHandler.
These interfaces are like contracts that say, “Hey, I’ll define what happens when dragging starts, continues, and ends!”
Replace your entire script with this:
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems; // Needed for event interfaces
public class InteractableObject : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("Interaction Settings")]
[SerializeField] private bool isTapInteractable = true; // Can this object be tapped?
[SerializeField] private bool isDragInteractable = false; // Can this object be dragged?
[SerializeField] private bool isCorrectAnswer = false; // Is this the correct tap solution?
[SerializeField] private InteractableObject correctDropTarget; // NEW: The target this object should be dropped on
// Internal references
private Image imageComponent;
private Color initialColor;
private Vector3 initialPosition;
private RectTransform rectTransform;
private void Awake()
{
imageComponent = GetComponent<Image>();
if (imageComponent != null)
initialColor = imageComponent.color;
rectTransform = GetComponent<RectTransform>();
initialPosition = rectTransform.position;
}
// --- Tap Interaction ---
public void OnPointerClick(PointerEventData eventData)
{
if (!isTapInteractable) return;
Debug.Log(gameObject.name + " was tapped!");
if (GameManager.Instance != null)
GameManager.Instance.HandleTapInteraction(this, isCorrectAnswer);
}
// --- Drag Interaction ---
public void OnBeginDrag(PointerEventData eventData)
{
if (!isDragInteractable) return;
Debug.Log(gameObject.name + " began dragging.");
transform.SetAsLastSibling();
// Optional: imageComponent.color = new Color(initialColor.r, initialColor.g, initialColor.b, 0.7f);
}
public void OnDrag(PointerEventData eventData)
{
if (!isDragInteractable) return;
rectTransform.position += (Vector3)eventData.delta;
}
public void OnEndDrag(PointerEventData eventData)
{
if (!isDragInteractable) return;
Debug.Log(gameObject.name + " ended dragging.");
imageComponent.color = initialColor;
if (correctDropTarget != null)
{
GameObject droppedOnObject = eventData.pointerEnter;
if (droppedOnObject != null)
{
InteractableObject targetInteractable = droppedOnObject.GetComponent<InteractableObject>();
if (targetInteractable == correctDropTarget)
{
Debug.Log(gameObject.name + " dropped on correct target: " + correctDropTarget.name);
GameManager.Instance.HandleDragDropInteraction(this, correctDropTarget, true);
return;
}
}
}
Debug.Log(gameObject.name + " dropped incorrectly.");
GameManager.Instance.HandleDragDropInteraction(this, correctDropTarget, false);
ResetVisuals();
}
// --- Public Methods ---
public void ResetVisuals()
{
if (imageComponent != null)
imageComponent.color = initialColor;
if (rectTransform != null)
rectTransform.position = initialPosition;
}
public void SetHighlightColor(Color color)
{
if (imageComponent != null)
imageComponent.color = color;
}
}
๐ง What’s New and Exciting?
✨ Interfaces: We’ve added IBeginDragHandler, IDragHandler, and IEndDragHandler.
๐ฎ isDragInteractable: Lets you toggle drag behavior in the Inspector.
๐ฏ correctDropTarget: Defines where each draggable should be dropped.
๐ initialPosition: Helps snap the object back if dropped incorrectly.
๐ฑ️ OnDrag: Moves the object with the pointer in real-time.
๐ OnEndDrag: Detects correct/incorrect drops and reports back to the GameManager.
Phew! That was a lot — but now your object is a true multitasker. Don’t forget to save your script and test it out! ๐งฉ
Step 2: GameManager – Our Puzzle Conductor
Our GameManager needs to understand this new language of drag and drop. It will be responsible for reacting to successful or unsuccessful drops.
Open GameManager.cs in your code editor.
We'll add a new method, HandleDragDropInteraction, and update ResetAllInteractables to be smarter about our puzzles.
Replace the entire content of your GameManager.cs script with the following code:
using UnityEngine;
using TMPro;
using System.Collections;
using System.Linq; // Needed for .Any() method
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("UI References")]
[SerializeField] private TextMeshProUGUI questionText; // Displays current puzzle question
[SerializeField] private TextMeshProUGUI feedbackText;
[SerializeField] private InteractableObject[] interactableObjects; // Changed from TapInteractable[]
[Header("Game Settings")]
[SerializeField] private float feedbackDisplayDuration = 2f;
[Header("Puzzle Configuration")]
[SerializeField] private PuzzleData[] allPuzzles; // Array holding all puzzle configurations
private int currentPuzzleIndex = 0; // Tracks which puzzle is currently active
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
InitializeUI();
LoadPuzzle(currentPuzzleIndex); // Load the first puzzle on start
}
private void InitializeUI()
{
if (feedbackText != null)
{
feedbackText.text = string.Empty;
feedbackText.gameObject.SetActive(false);
}
else
{
Debug.LogError("Feedback Text is not assigned in GameManager!");
}
if (questionText == null)
{
Debug.LogError("Question Text is not assigned in GameManager!");
}
}
// Handles tap interactions (called by InteractableObject)
public void HandleTapInteraction(InteractableObject tappedObject, bool isCorrect)
{
if (feedbackText.gameObject.activeSelf) return;
if (isCorrect)
{
ShowFeedback("CORRECT!", Color.green);
StartCoroutine(HideFeedbackAfterDelay(true)); // Move to next puzzle after correct answer
}
else
{
ShowFeedback("TRY AGAIN!", Color.red);
tappedObject.SetHighlightColor(Color.red);
StartCoroutine(ResetPuzzleAfterDelay());
}
}
// Handles drag & drop interactions (called by InteractableObject)
public void HandleDragDropInteraction(InteractableObject draggedObject, InteractableObject targetObject, bool isCorrectDrop)
{
if (feedbackText.gameObject.activeSelf) return;
if (isCorrectDrop)
{
ShowFeedback("CORRECT!", Color.green);
StartCoroutine(HideFeedbackAfterDelay(true));
}
else
{
ShowFeedback("TRY AGAIN!", Color.red);
StartCoroutine(ResetPuzzleAfterDelay());
}
}
private void ShowFeedback(string message, Color color)
{
if (feedbackText != null)
{
feedbackText.gameObject.SetActive(true);
feedbackText.text = message;
feedbackText.color = color;
}
}
private IEnumerator HideFeedbackAfterDelay(bool loadNext)
{
yield return new WaitForSeconds(feedbackDisplayDuration);
if (feedbackText != null)
{
feedbackText.gameObject.SetActive(false);
}
if (loadNext)
{
currentPuzzleIndex++;
if (currentPuzzleIndex < allPuzzles.Length)
{
LoadPuzzle(currentPuzzleIndex);
}
else
{
Debug.Log("All puzzles completed!");
ShowFeedback("Game Completed!", Color.yellow);
}
}
ResetAllInteractables();
}
private IEnumerator ResetPuzzleAfterDelay()
{
yield return new WaitForSeconds(feedbackDisplayDuration);
if (feedbackText != null)
{
feedbackText.gameObject.SetActive(false);
}
ResetAllInteractables();
}
private void ResetAllInteractables()
{
if (allPuzzles != null && allPuzzles.Length > currentPuzzleIndex && allPuzzles[currentPuzzleIndex].interactableObjects != null)
{
foreach (InteractableObject obj in allPuzzles[currentPuzzleIndex].interactableObjects)
{
if (obj != null)
{
obj.ResetVisuals();
}
}
}
}
// Loads a specific puzzle configuration
private void LoadPuzzle(int index)
{
if (index < 0 || index >= allPuzzles.Length)
{
Debug.LogError("Attempted to load invalid puzzle index: " + index);
return;
}
PuzzleData puzzleToLoad = allPuzzles[index];
// Hide all existing interactable objects
foreach (InteractableObject obj in interactableObjects)
{
if (obj != null) obj.gameObject.SetActive(false);
}
// Set question text
if (questionText != null)
{
questionText.text = puzzleToLoad.question;
}
// Activate and configure interactable objects for this puzzle
foreach (InteractableObject obj in puzzleToLoad.interactableObjects)
{
if (obj != null)
{
obj.gameObject.SetActive(true);
obj.ResetVisuals();
}
}
Debug.Log($"Loaded Puzzle {index + 1}: {puzzleToLoad.question}");
interactableObjects = puzzleToLoad.interactableObjects;
}
}
Key Updates in GameManager:
-
HandleDragDropInteraction: New method called byInteractableObjectto handle drop results. -
questionText: Added to display each puzzle’s question. -
allPuzzles: StoresPuzzleDataScriptable Objects for dynamic loading. -
currentPuzzleIndex: Tracks which puzzle is currently active. -
LoadPuzzle(int index): Hides old puzzle objects, sets new question, and loads relevant interactables. -
ResetAllInteractables: Now resets only objects relevant to the current puzzle.
Remember to save your script after making these changes.
Step 3: PuzzleData – The Brains Behind the Puzzles
This is where things get really cool for managing your game content. Instead of hardcoding each puzzle, we'll create ScriptableObject assets that define our puzzles. Think of them as blueprints for each level.
3.1 Create the PuzzleData Script
-
In your
_Scriptsfolder (or create a new_PuzzleDatafolder for organization), right-click, then go to Create > C# Script. -
Name it
PuzzleData.
3.2 Open and Configure PuzzleData.cs
Open PuzzleData.cs and paste the following code:
using UnityEngine;
[CreateAssetMenu(fileName = "NewPuzzle", menuName = "TrickyLogicPuzzle/Puzzle Data")]
public class PuzzleData : ScriptableObject
{
public string puzzleName = "Unnamed Puzzle";
[TextArea(3, 5)] // Allows multiline text in inspector
public string question = "Solve this puzzle!";
public InteractableObject[] interactableObjects; // The objects relevant to THIS puzzle
// public InteractableObject[] correctTapObjects; // Could have specific correct tap objects
// public DropTarget[] dropTargets; // Could have specific drop targets
// Add more puzzle specific data here as needed, e.g., background image, hint text
}
What's Happening Here?
-
[CreateAssetMenu(...)]– Adds an entry to Unity's Create menu, allowing you to makePuzzleDataassets directly in the Project window. -
ScriptableObject– A data container in Unity that doesn’t need to be attached to a GameObject. Perfect for defining puzzles, item stats, or configurations. -
puzzleName&question– Simple string fields for labeling and describing puzzles.[TextArea]makesquestionmulti-line in the Inspector. -
interactableObjects– An array to assign allInteractableObjects belonging to this specific puzzle.
Save your PuzzleData.cs script.
Step 4: Configuring Unity Objects for Drag & Drop
Now it’s time to set up our scene and bring the drag-and-drop system to life.
4.1 Add New UI Elements
Let’s create a draggable object and a drop target.
1. Add a Drag-Me Image
-
In the Hierarchy, right-click on your Canvas.
-
Select UI > Image.
-
Rename it
DragMeObject. -
In the Inspector, set the
Rect Transformproperties:-
Pos X:
0 -
Pos Y:
-600 -
Width:
200 -
Height:
200
-
-
For the Image (Script), select a noticeable color (e.g., Magenta).
-
Ensure Raycast Target is checked — this enables pointer detection.
2. Add a Drop Target Image
-
In the Hierarchy, right-click on your Canvas.
-
Select UI > Image.
-
Rename it
DropTargetObject. -
In the Inspector, set the
Rect Transformproperties:-
Pos X:
0 -
Pos Y:
200 -
Width:
300 -
Height:
300
-
-
For the Image (Script), select a distinct color (e.g., Cyan).
-
Ensure Raycast Target is checked — this allows other objects to be dropped onto it.
You should now see two new shapes on your Canvas.
4.2 Configure Your InteractableObject Scripts
Now we need to define how each object behaves by adjusting its InteractableObject component in the Inspector.
Select Each Object in the Hierarchy and Configure as Follows:
1. SquareObject
-
Is Tap Interactable: Checked
-
Is Drag Interactable: Unchecked
-
Is Correct Answer: Unchecked
-
Correct Drop Target: Leave None
4.3 Configure the Remaining InteractableObject Scripts
Now, let’s finish configuring the rest of your interactable objects in the Unity Inspector.
Select CircleObject:
-
Is Tap Interactable: CHECKED
-
Is Drag Interactable: CHECKED (Let’s make it draggable too, just to test it in action, though it won’t be the correct drop target for this puzzle.)
-
Is Correct Answer: CHECKED (This remains our correct tap answer for the first puzzle.)
-
Correct Drop Target: Leave None
Select DragMeObject:
-
Is Tap Interactable: UNCHECKED
-
Is Drag Interactable: CHECKED
-
Is Correct Answer: UNCHECKED
-
Correct Drop Target: Drag
DropTargetObject(from your Hierarchy) into this field — this step is crucial!
4.4 Configure the GameManager for Puzzles
We’re almost done! Now it’s time to connect everything so the _GameManager knows about our newly created puzzles.
-
Select the
_GameManagerobject in the Hierarchy. -
In the Inspector, under the
GameManager (Script)component, configure the following:-
Question Text: Drag your
QuestionText(from the Hierarchy) into this slot.-
(If you don’t have one yet, right-click on the Canvas → UI > Text - TextMeshPro and assign it here.)
-
-
All Puzzles: Set Size to
2. -
Element 0: Drag
Puzzle1_TapTheCircle(from your Project window) into this field. -
Element 1: Drag
Puzzle2_DragToTarget(from your Project window) into this field.
-
4.5 Hide All Puzzle Objects Initially
Since our GameManager will activate objects dynamically at runtime, we should start with all puzzle objects hidden.
-
In the Hierarchy, select the following objects:
-
SquareObject -
CircleObject -
DragMeObject -
DropTargetObject
-
-
Uncheck the checkbox next to each of their names in the Hierarchy.
-
This makes them inactive at the start of the game.

Comments
Post a Comment