Step-by-Step guide on How to Implement Swipe Interactions and Hidden Objects in Unity (Tricky Logic Puzzles)

 


Step-by-Step guide How to Implement Swipe Interactions and Hidden Objects in Unity (Tricky Logic Puzzles)

Ready to add even more dynamic and engaging puzzles to your Unity game? This tutorial will guide you through implementing two crucial mechanics:

  1. Swipe Interactions: Allowing players to "swipe" objects off-screen to trigger events.

  2. Hidden Object Logic: Revealing new interactive elements only after specific actions are performed.

These features are fantastic for creating layers of discovery and making your puzzles feel more interactive and mysterious, similar to popular "Brain Test" style games.

Estimated Time: 45-60 minutes
Difficulty: Intermediate
Prerequisites: Basic Unity, C# scripting, and your existing "Tricky Logic Puzzle" project setup (from previous steps).

Let's expand your puzzle-making toolkit!

Step 1: Implement Swipe Interaction in InteractableObject

We'll enhance our InteractableObject script to detect a "swipe" action, which is essentially a drag that moves an object sufficiently far in a specific direction.

Modify 

  1. Open InteractableObject.cs.

  2. Replace its entire content with the updated code provided below.

Key Changes:

  • : A new enum value to categorize swipeable objects.

  •  Header: New serialized fields (swipeThreshold, swipeDuration, swipeDirection) to customize swipe behavior in the Inspector.

  • : A GameObject reference that will become active when this object is successfully swiped away.

  •  Update: Now includes logic to check if a drag qualifies as a "swipe" based on distance and direction. If it does, it animates the object off-screen and activates objectToRevealOnSwipe.

codeC#

using UnityEngine;

using UnityEngine.UI;

using UnityEngine.EventSystems;

using DG.Tweening;


public enum InteractableObjectType

{

    Tap_SingleCorrect,

    Drag_ToTarget,

    Tap_SequencePart,

    Tap_FakeWrong,

    Swipe_OffScreen, // NEW: For objects that can be swiped away

    NonInteractable

}


public class InteractableObject : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler

{

    [Header("Interaction Settings")]

    [SerializeField] private InteractableObjectType interactionType = InteractableObjectType.Tap_SingleCorrect;

    [SerializeField] private InteractableObject correctDropTarget; // Only relevant for Drag_ToTarget


    [Header("Sequence Specifics (for Tap_SequencePart)")]

    public int sequenceOrder = -1;


    [Header("Swipe Specifics (for Swipe_OffScreen)")] // NEW

    [SerializeField] private float swipeThreshold = 300f; // How far to drag to count as a swipe

    [SerializeField] private float swipeDuration = 0.3f; // How fast it animates off screen

    [SerializeField] private Vector2 swipeDirection = new Vector2(1, 0); // e.g., (1,0) for right, (-1,0) for left, (0,1) for up

    public GameObject objectToRevealOnSwipe; // NEW: Object to activate when this is swiped away


    [Header("Animation Settings")]

    [SerializeField] private float wiggleDuration = 0.2f;

    [SerializeField] private float wiggleStrength = 10f;

    [SerializeField] private int wiggleVibrato = 10;

    [SerializeField] private float wiggleElasticity = 1f;


    // Internal references

    private Image imageComponent;

    private Color initialColor;

    private Vector3 initialPosition;

    private Quaternion initialRotation;

    private RectTransform rectTransform;

    private Vector3 dragStartPos; // NEW: To track where drag began for swipe detection


    private void Awake()

    {

        imageComponent = GetComponent<Image>();

        if (imageComponent != null)

        {

            initialColor = imageComponent.color;

        }

        rectTransform = GetComponent<RectTransform>();

        initialPosition = rectTransform.position;

        initialRotation = rectTransform.localRotation;

    }


    public void OnPointerClick(PointerEventData eventData)

    {

        if (interactionType == InteractableObjectType.NonInteractable) return;

        if (GameManager.Instance.IsDragging) return


        Debug.Log(gameObject.name + " was tapped!");

        GameManager.Instance.HandleTapInteraction(this);

    }


    public void OnBeginDrag(PointerEventData eventData)

    {

        if (interactionType != InteractableObjectType.Drag_ToTarget && interactionType != InteractableObjectType.Swipe_OffScreen) return;


        Debug.Log(gameObject.name + " began dragging.");

        transform.SetAsLastSibling();

        GameManager.Instance.IsDragging = true;

        dragStartPos = rectTransform.position; 

    }


    public void OnDrag(PointerEventData eventData)

    {

        if (interactionType != InteractableObjectType.Drag_ToTarget && interactionType != InteractableObjectType.Swipe_OffScreen) return;

        rectTransform.position += (Vector3)eventData.delta;

    }


    public void OnEndDrag(PointerEventData eventData)

    {

        if (interactionType != InteractableObjectType.Drag_ToTarget && interactionType != InteractableObjectType.Swipe_OffScreen) return;


        Debug.Log(gameObject.name + " ended dragging.");

        imageComponent.color = initialColor;

        GameManager.Instance.IsDragging = false;


        bool isCorrectInteraction = false;

        

        if (interactionType == InteractableObjectType.Drag_ToTarget)

        {

            if (correctDropTarget != null)

            {

                GameObject droppedOnObject = eventData.pointerEnter;

                if (droppedOnObject != null && droppedOnObject.GetComponent<InteractableObject>() == correctDropTarget)

                {

                    isCorrectInteraction = true;

                    Debug.Log(gameObject.name + " was dropped on its correct target: " + correctDropTarget.name);

                }

            }

            GameManager.Instance.HandleDragDropInteraction(this, correctDropTarget, isCorrectInteraction);

            if (!isCorrectInteraction) ResetVisuals(); // Snap back only if incorrect

        }

        else if (interactionType == InteractableObjectType.Swipe_OffScreen)

        {

            Vector3 dragDistance = rectTransform.position - dragStartPos;

            Vector3 projectedDistance = Vector3.Project(dragDistance, swipeDirection.normalized);


            if (projectedDistance.magnitude >= swipeThreshold && Vector3.Dot(dragDistance.normalized, swipeDirection.normalized) > 0.5f)

            {

                isCorrectInteraction = true;

                Debug.Log(gameObject.name + " swiped off screen in correct direction!");

                rectTransform.DOMove(rectTransform.position + (Vector3)swipeDirection.normalized * 1000f, swipeDuration)

                             .SetEase(Ease.OutQuad)

                             .OnComplete(() =>

                             {

                                 gameObject.SetActive(false);

                                 if (objectToRevealOnSwipe != null)

                                 {

                                     objectToRevealOnSwipe.SetActive(true);

                                 }

                             });

            }

            else

            {

                isCorrectInteraction = false;

            }

            GameManager.Instance.HandleSwipeInteraction(this, isCorrectInteraction);

            if (!isCorrectInteraction) ResetVisuals(); // Snap back if not swiped correctly

        }

    }


    public void ResetVisuals()

    {

        if (imageComponent != null)

        {

            imageComponent.color = initialColor;

        }

        if (rectTransform != null)

        {

            rectTransform.DOKill(true);

            rectTransform.position = initialPosition;

            rectTransform.localRotation = initialRotation;

        }

    }


    public void SetHighlightColor(Color color, bool doWiggle = false)

    {

        if (imageComponent != null)

        {

            imageComponent.color = color;

        }

        if (doWiggle)

        {

            Wiggle();

        }

    }


    private void Wiggle()

    {

        rectTransform.DOKill();

        rectTransform.DOPunchRotation(

            new Vector3(0f, 0f, wiggleStrength),

            wiggleDuration,

            wiggleVibrato,

            wiggleElasticity

        ).SetEase(Ease.OutElastic)

         .OnComplete(() => rectTransform.localRotation = initialRotation);

    }


    public InteractableObjectType GetInteractionType() => interactionType;

    public int GetSequenceOrder() => sequenceOrder;

}

  1. Save the InteractableObject.cs script.

Step 2: Update GameManager for Swipe and Revealing Objects

The GameManager needs to recognize and respond to swipe interactions, similar to how it handles taps and drags.

Modify 

  1. Open GameManager.cs.

  2. Replace its entire content with the updated code below.

Key Changes:

  • : A new public method called by InteractableObject when a swipe occurs. It processes the swipe result and updates puzzle progress.

  •  Logic: Adjusted to correctly handle Swipe_OffScreen objects. For successful swipes, the object is already animated away and hidden by its own script, so ResetVisuals isn't called on it immediately.

  •  Refinement: The Tap_SingleCorrect case now explicitly checks !currentPuzzle.isMultiStagePuzzle to avoid conflicts with multi-stage sequences where "single correct taps" might be steps, not full solutions.

codeC#

using UnityEngine;

using TMPro;

using System.Collections;

using System.Linq; 


public class GameManager : MonoBehaviour

{

    public static GameManager Instance { get; private set; }


    [Header("UI References")]

    [SerializeField] private TextMeshProUGUI questionText;

    [SerializeField] private TextMeshProUGUI feedbackText;

    [SerializeField] private TextMeshProUGUI hintText;

    [SerializeField] private GameObject hintPanel;

    [SerializeField] private InteractableObject[] allSceneInteractableObjects; // Array of ALL interactable objects in the scene


    [Header("Audio References")]

    [SerializeField] private AudioSource audioSource;

    [SerializeField] private AudioClip correctSound;

    [SerializeField] private AudioClip incorrectSound;

    [SerializeField] private AudioClip tapSound;


    [Header("Game Settings")]

    [SerializeField] private float feedbackDisplayDuration = 2f;


    [Header("Puzzle Configuration")]

    [SerializeField] private PuzzleData[] allPuzzles;

    private int currentPuzzleIndex = 0;


    // For Multi-Stage Puzzles

    private int currentCorrectInteractions = 0;

    private int currentSequenceStep = 0;

    public bool IsDragging { get; set; } = false


    private void Awake()

    {

        if (Instance != null && Instance != this)

        {

            Destroy(gameObject);

            return;

        }

        Instance = this;


        InitializeUI();

        LoadPuzzle(currentPuzzleIndex);

    }


    private void InitializeUI()

    {

        feedbackText?.gameObject.SetActive(false);

        if (feedbackText == null) Debug.LogError("Feedback Text is not assigned in GameManager!");

        if (questionText == null) Debug.LogError("Question Text is not assigned in GameManager!");

        hintPanel?.SetActive(false);

        if (hintText == null) Debug.LogError("Hint Text is not assigned in GameManager!");

        if (audioSource == null)

        {

            audioSource = GetComponent<AudioSource>();

            if (audioSource == null) Debug.LogError("AudioSource not found on GameManager! Please add one.");

        }

    }


    public void HandleTapInteraction(InteractableObject tappedObject)

    {

        if (feedbackText.gameObject.activeSelf || IsDragging) return;

        PlaySound(tapSound);

        HideHint();


        bool isCorrect = false;

        PuzzleData currentPuzzle = allPuzzles[currentPuzzleIndex];


        switch (tappedObject.GetInteractionType())

        {

            case InteractableObjectType.Tap_SingleCorrect:

                isCorrect = currentPuzzle.interactableObjects.Contains(tappedObject) && !currentPuzzle.isMultiStagePuzzle;

                break;


            case InteractableObjectType.Tap_SequencePart:

                if (currentPuzzle.isMultiStagePuzzle && currentPuzzle.sequenceCorrectOrder.Length > currentSequenceStep &&

                    tappedObject == currentPuzzle.sequenceCorrectOrder[currentSequenceStep])

                {

                    isCorrect = true;

                    tappedObject.SetHighlightColor(Color.green);

                    currentSequenceStep++;

                }

                else

                {

                    isCorrect = false;

                }

                break;


            case InteractableObjectType.Tap_FakeWrong:

                isCorrect = false;

                break;


            // Tapping these types is generally incorrect for puzzle solution unless specific logic overrides

            case InteractableObjectType.Drag_ToTarget:

            case InteractableObjectType.Swipe_OffScreen:

            case InteractableObjectType.NonInteractable:

                isCorrect = false;

                break;

        }

        ProcessInteractionResult(isCorrect, tappedObject);

    }


    public void HandleDragDropInteraction(InteractableObject draggedObject, InteractableObject targetObject, bool isCorrectDrop)

    {

        if (feedbackText.gameObject.activeSelf) return;

        PlaySound(tapSound); 

        HideHint();


        bool isCorrect = isCorrectDrop && draggedObject.GetInteractionType() == InteractableObjectType.Drag_ToTarget;

        ProcessInteractionResult(isCorrect, draggedObject);

    }


    public void HandleSwipeInteraction(InteractableObject swipedObject, bool isCorrectSwipe)

    {

        if (feedbackText.gameObject.activeSelf) return;

        PlaySound(tapSound); 

        HideHint();


        bool isCorrect = isCorrectSwipe && swipedObject.GetInteractionType() == InteractableObjectType.Swipe_OffScreen;

        ProcessInteractionResult(isCorrect, swipedObject);

    }


    private void ProcessInteractionResult(bool isCorrect, InteractableObject interactedObject = null)

    {

        if (isCorrect)

        {

            PlaySound(correctSound);

            ShowFeedback("CORRECT!", Color.green);

            currentCorrectInteractions++;

            

            if (currentCorrectInteractions >= allPuzzles[currentPuzzleIndex].totalCorrectInteractionsNeeded)

            {

                StartCoroutine(HideFeedbackAfterDelay(true));

            }

            else

            {

                StartCoroutine(HideFeedbackAfterDelay(false));

            }

        }

        else

        {

            PlaySound(incorrectSound);

            ShowFeedback("TRY AGAIN!", Color.red);

            interactedObject?.SetHighlightColor(Color.red, true);

            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);

        feedbackText?.gameObject.SetActive(false);


        if (loadNext)

        {

            currentPuzzleIndex++;

            if (currentPuzzleIndex < allPuzzles.Length)

            {

                LoadPuzzle(currentPuzzleIndex);

            }

            else

            {

                Debug.Log("All puzzles completed!");

                ShowFeedback("Game Completed!", Color.yellow);

            }

        }

        // Reset all interactables, ensuring swipe-away objects stay hidden

        ResetAllInteractables(); 

    }


    private IEnumerator ResetPuzzleAfterDelay()

    {

        yield return new WaitForSeconds(feedbackDisplayDuration);

        feedbackText?.gameObject.SetActive(false);

        ResetAllInteractables();

    }


    private void ResetAllInteractables()

    {

        if (allSceneInteractableObjects == null) return;

        foreach (InteractableObject obj in allSceneInteractableObjects)

        {

            if (obj != null)

            {

                obj.ResetVisuals();

            }

        }

        currentCorrectInteractions = 0;

        currentSequenceStep = 0;

        IsDragging = false;

    }


    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];


        currentCorrectInteractions = 0;

        currentSequenceStep = 0;

        IsDragging = false;


        foreach (InteractableObject obj in allSceneInteractableObjects)

        {

            if (obj != null) obj.gameObject.SetActive(false);

        }


        questionText.text = puzzleToLoad.question;


        foreach (InteractableObject obj in puzzleToLoad.interactableObjects)

        {

            if (obj != null)

            {

                obj.gameObject.SetActive(true);

                obj.ResetVisuals(); 

            }

        }

        

        hintText.text = puzzleToLoad.hint;

        HideHint();


        Debug.Log($"Loaded Puzzle {index + 1}: {puzzleToLoad.question}");

    }


    public void ShowHint()

    {

        if (hintPanel != null && allPuzzles.Length > currentPuzzleIndex && allPuzzles[currentPuzzleIndex].hint != "")

        {

            hintPanel.SetActive(true);

        }

    }


    public void HideHint() => hintPanel?.SetActive(false);


    private void PlaySound(AudioClip clip)

    {

        if (audioSource != null && clip != null)

        {

            audioSource.PlayOneShot(clip);

        }

    }

}

  1. Save the GameManager.cs script.

Step 3: Create New Objects and Configure Puzzles in Unity

Now let's build a new puzzle (Puzzle 5) that uses these swipe and reveal mechanics.

A. Create a Swipeable Object

This will be the "cover" that needs to be swiped away.

  1. In the Hierarchy, right-click on Canvas.

  2. Select UI > Image. Rename it SwipeAwayBlock.

  3. In the Inspector, set its properties:

    • Rect Transform: Pos X: 0, Pos Y: 0, Width: 300, Height: 300

    • Image (Script): Color: Black


  4. Add an InteractableObject component.

    • Interaction Type: Swipe_OffScreen

    • Swipe Threshold: 300

    • Swipe Direction: X: 0, Y: -1 (This means swipe downwards).

Object To Reveal On Swipe: Leave this empty for now; we'll assign our hidden button here shortly.

Comments

Popular posts from this blog

Step-by-Step Guide on How to Create a GDD (Game Design Document)

Unity Scriptable Objects: A Step-by-Step Tutorial

Unity 2D Tilemap Tutorial for Procedural Level Generation