Level Up Your Unity Puzzles: A Step-by-Step Guide to Number & Text Manipulation

 

Level Up Your Unity Puzzles: A Step-by-Step Guide to Number & Text Manipulation

Tired of static puzzles in your Unity game? Want to inject more dynamic interaction and brain-teasing challenges? You're in luck! This guide will walk you through how to implement powerful new mechanics in your Unity game, allowing players to manipulate numbers, compare values, and even drag words directly from the question text!

We'll cover a step-by-step guide on how to create these engaging interactions, focusing on clean code and clear explanations. Get ready to elevate your puzzle game to the next level!

Why Add Number and Text Manipulation?

Traditional tap-and-solve puzzles are fun, but imagine the possibilities with dynamic elements:

  • Numerical Challenges: Players can tap objects to change values, then use those numbers to solve a math puzzle or a sequence challenge.

  • Object Comparison: Ask players to find the "smallest" or "largest" number among a group, adding a layer of observational and analytical thinking.

  • Interactive Questions: Instead of just reading, players can directly interact with the question text itself, dragging out keywords to reveal hidden meanings or solve riddles.

These mechanics add depth, replayability, and that satisfying "Aha!" moment players crave.

Phase 1: Making Text Interactive with InteractableText

First, let's enable dragging parts of our UI text.

Step-by-step guide on how to create InteractableText:

  1. Create the Script: In your Unity project, navigate to your _Scripts folder. Right-click > Create > C# Script and name it InteractableText.

  2. Add the Code: Open InteractableText.cs and paste the following code. This script will handle the core drag-and-drop functionality for text elements.

    C#
    using UnityEngine;
    using TMPro;
    using UnityEngine.EventSystems;
    using DG.Tweening; // Ensure you have DOTween imported for animations
    
    public enum InteractableTargetType
    {
        None,
        ObjectTarget,
        QuestionText,
        HintButton,
        CoverSpot,
        InvisibleTrigger,
        TextSegment      // New: For a specific segment of text
    }
    
    public class InteractableText : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
    {
        [Header("Text Interaction Settings")]
        [SerializeField] private InteractableTargetType targetType = InteractableTargetType.None;
        [SerializeField] private string correctTextContent = "";
        [SerializeField] private bool canBeDragged = false;
    
        private RectTransform rectTransform;
        private Vector3 initialPosition;
        private TextMeshProUGUI textMeshPro;
    
        private void Awake()
        {
            rectTransform = GetComponent<RectTransform>();
            initialPosition = rectTransform.position;
            textMeshPro = GetComponent<TextMeshProUGUI>();
            if (textMeshPro == null) { Debug.LogError("InteractableText: TextMeshProUGUI component not found!"); }
        }
    
        public void OnBeginDrag(PointerEventData eventData)
        {
            if (!canBeDragged) return;
            transform.SetAsLastSibling(); // Bring to front
            GameManager.Instance.IsDragging = true;
        }
    
        public void OnDrag(PointerEventData eventData)
        {
            if (!canBeDragged) return;
            rectTransform.position += (Vector3)eventData.delta;
        }
    
        public void OnEndDrag(PointerEventData eventData)
        {
            if (!canBeDragged) return;
            GameManager.Instance.IsDragging = false;
            // Report to GameManager to handle puzzle logic
            GameManager.Instance.HandleTextDragInteraction(this, true);
        }
    
        public void ResetVisuals()
        {
            if (rectTransform != null)
            {
                rectTransform.DOKill(true);
                rectTransform.position = initialPosition;
            }
        }
    
        public string GetCorrectTextContent() => correctTextContent;
        public InteractableTargetType GetTargetType() => targetType;
        public bool CanBeDragged() => canBeDragged;
    }
  3. Save the Script: Ensure you save InteractableText.cs.

Phase 2: Enhancing Interactable Objects and Game Management

Now, let's update our existing InteractableObject to support numerical values and integrate these new features into our GameManager.

Step-by-step guide on how to modify existing scripts:

  1. Modify 

    • Open InteractableObject.cs.

    • Add new Enums: Under InteractableObjectType, add:

      C#
      Compare_Value,      // Object used in numerical comparison puzzles (e.g., smallest/largest)
      Tap_ChangeValue,    // Tapping this object changes its numerical value
    • Add Numerical Fields: Under a new [Header("Value/Text Specifics (NEW)")] section, add:

      C#
      [Header("Value/Text Specifics (NEW)")]
      [SerializeField] private int numericalValue = 0;
      [SerializeField] private TextMeshProUGUI valueTextDisplay;
      [SerializeField] private int valueIncrement = 1;
      [SerializeField] private int valueMin = 0;
      [SerializeField] private int valueMax = 9;
      private int initialNumericalValue;
    • Update  Initialize initialNumericalValue and call UpdateValueDisplay():

      C#
      private void Awake()
      {
          // ... existing Awake code ...
          if (valueTextDisplay != null)
          {
              initialNumericalValue = numericalValue;
              UpdateValueDisplay();
          }
      }
    • Modify  Handle Tap_ChangeValue:

      C#
      public void OnPointerClick(PointerEventData eventData)
      {
          // ... existing OnPointerClick code ...
          if (interactionType == InteractableObjectType.Tap_ChangeValue)
          {
              ChangeValue();
          }
          GameManager.Instance.HandleTapInteraction(this);
      }
    • Modify  Allow Compare_Value objects to be dragged:

      C#
      // In OnBeginDrag and OnEndDrag, find the line that checks interactionType:
      if (interactionType != InteractableObjectType.Drag_ToTarget && 
          // ... other types ...
          interactionType != InteractableObjectType.Compare_Value) return; // Add this line
    • Add 

      C#
      private void ChangeValue()
      {
          numericalValue += valueIncrement;
          if (numericalValue > valueMax) { numericalValue = valueMin; }
          if (numericalValue < valueMin) { numericalValue = valueMax; }
          UpdateValueDisplay();
          Debug.Log($"{gameObject.name} value changed to {numericalValue}");
      }
      
      private void UpdateValueDisplay()
      {
          if (valueTextDisplay != null)
          {
              valueTextDisplay.text = numericalValue.ToString();
          }
      }
      public int GetNumericalValue() => numericalValue; // New Getter
    • Update  Reset numerical value:

      C#
      public void ResetVisuals()
      {
          // ... existing ResetVisuals code ...
          if (valueTextDisplay != null)
          {
              numericalValue = initialNumericalValue;
              UpdateValueDisplay();
          }
      }
    • Save InteractableObject.cs.

  2. Modify 

    • Open GameManager.cs.

    • Add 

      C#
      [SerializeField] private InteractableText[] allSceneInteractableTexts; // NEW: For draggable text segments
    • Add 

      C#
      [SerializeField] private AudioClip numberChangeSound; // NEW: For changing numerical values
    • Update  Add cases for Tap_ChangeValue and Compare_Value:

      C#
      public void HandleTapInteraction(InteractableObject tappedObject)
      {
          // ... existing code ...
          switch (tappedObject.GetInteractionType())
          {
              // ... existing cases ...
              case InteractableObjectType.Tap_ChangeValue:
                  PlaySound(numberChangeSound);
                  if (CheckNumericalValueSolution(tappedObject)) { isCorrect = true; } else { return; } // Don't fail immediately
                  break;
              case InteractableObjectType.Compare_Value:
                  PlaySound(tapSound);
                  if (currentPuzzle.comparisonType == ComparisonType.Smallest) {
                      if (CheckNumericalComparisonSolution(tappedObject, ComparisonType.Smallest)) { isCorrect = true; }
                  } else if (currentPuzzle.comparisonType == ComparisonType.Largest) {
                      if (CheckNumericalComparisonSolution(tappedObject, ComparisonType.Largest)) { isCorrect = true; }
                  }
                  break;
              // ... default case ...
          }
          ProcessInteractionResult(isCorrect, tappedObject);
      }
    • Add 

      C#
      public void HandleTextDragInteraction(InteractableText draggedText, bool isCorrectDrag)
      {
          if (feedbackText.gameObject.activeSelf) return;
          PlaySound(uiInteractionSound); // Or a specific text-drag sound
          HideHint();
      
          bool isCorrect = isCorrectDrag &&
                           allPuzzles[currentPuzzleIndex].interactableTexts.Contains(draggedText) &&
                           draggedText.GetTargetType() == InteractableTargetType.TextSegment;
      
          ProcessInteractionResult(isCorrect, null);
          if (isCorrect) { draggedText.gameObject.SetActive(false); } // Hide the text if correct
      }
    • Add 

      C#
      public enum ComparisonType { None, Smallest, Largest } // NEW Enum
      
      private bool CheckNumericalComparisonSolution(InteractableObject tappedObject, ComparisonType requiredComparison)
      {
          PuzzleData currentPuzzle = allPuzzles[currentPuzzleIndex];
          if (currentPuzzle.comparisonType != requiredComparison) return false;
      
          var comparableObjects = currentPuzzle.interactableObjects
                                  .Where(o => o.GetInteractionType() == InteractableObjectType.Compare_Value)
                                  .ToList();
      
          if (!comparableObjects.Any()) return false;
      
          int extremumValue = requiredComparison == ComparisonType.Smallest ? int.MaxValue : int.MinValue;
          InteractableObject extremumObject = null;
      
          foreach (var obj in comparableObjects)
          {
              int val = obj.GetNumericalValue();
              if (requiredComparison == ComparisonType.Smallest)
              {
                  if (val < extremumValue) { extremumValue = val; extremumObject = obj; }
              }
              else // Largest
              {
                  if (val > extremumValue) { extremumValue = val; extremumObject = obj; }
              }
          }
          return tappedObject == extremumObject;
      }
      
      private bool CheckNumericalValueSolution(InteractableObject tappedObject)
      {
          PuzzleData currentPuzzle = allPuzzles[currentPuzzleIndex];
          return tappedObject.GetInteractionType() == InteractableObjectType.Tap_ChangeValue &&
                 tappedObject.GetNumericalValue() == currentPuzzle.correctNumericalValue;
      }
    • Update  Call ResetAllInteractableTexts():

      C#
      private IEnumerator HideFeedbackAfterDelay(bool loadNext)
      {
          // ... existing code ...
          ResetAllInteractables();
          ResetAllInteractableTexts(); // NEW
      }
      
      private IEnumerator ResetPuzzleAfterDelay()
      {
          // ... existing code ...
          ResetAllInteractables();
          ResetAllInteractableTexts(); // NEW
      }
    • Add 

      C#
      private void ResetAllInteractableTexts()
      {
          if (allSceneInteractableTexts != null)
          {
              foreach (InteractableText txt in allSceneInteractableTexts)
              {
                  if (txt != null) { txt.ResetVisuals(); txt.gameObject.SetActive(false); }
              }
          }
      }
    • Update  Deactivate all InteractableText objects and then activate those for the current puzzle.

      C#
      private void LoadPuzzle(int index)
      {
          // ... existing code for deactivating interactable objects ...
          foreach (InteractableText txt in allSceneInteractableTexts) { if (txt != null) { txt.gameObject.SetActive(false); } } // NEW
      
          // ... existing code for activating puzzle's interactable objects ...
      
          // Activate and reset visuals for current puzzle's interactable texts (NEW)
          foreach (InteractableText txt in puzzleToLoad.interactableTexts)
          {
              if (txt != null)
              {
                  txt.gameObject.SetActive(true);
                  txt.ResetVisuals();
              }
          }
          // ... rest of LoadPuzzle code ...
      }
    • Save GameManager.cs.

Phase 3: Defining New Puzzle Data

Our PuzzleData Scriptable Object needs to accommodate these new puzzle types.

Step-by-step guide on how to update PuzzleData.cs:

  1. Modify 

    • Open PuzzleData.cs.

    • Add 

      C#
      public InteractableText[] interactableTexts; // NEW: For draggable text segments
    • Add Numerical Puzzle Fields:

      C#
      [Header("Numerical Puzzle (NEW)")]
      public GameManager.ComparisonType comparisonType = GameManager.ComparisonType.None; // For comparing values
      public int correctNumericalValue = 0; // For puzzles where an object needs to reach a specific number
    • Save PuzzleData.cs.

Phase 4: Setting Up New Puzzles in Unity

Now for the exciting part: creating the actual puzzles! We'll set up three examples: number changing, smallest number, and dragging text.

Step-by-step guide on how to create and configure assets:

  1. Add Number Change Sound:

    • In your _Audio folder, import a short sound clip (e.g., a "blip" or "click").

    • Select _GameManager in your Hierarchy.

    • Drag the new audio clip into the Number Change Sound slot in the Inspector.

  2. Create Number Changing Object (Puzzle 12):

    • Right-click Canvas > UI > Image. Rename it NumberBlock.

    • Set its Rect Transform (e.g., X: 0, Y: 0, Width/Height: 150). Choose a light color.

    • Right-click NumberBlock > UI > Text - TextMeshPro. Rename it ValueText.

    • Set ValueText's Rect Transform (e.g., X: 0, Y: 0, Width/Height: 150). Set its text to "0", font size to 80, and alignment to center.

    • Select NumberBlock. Add Component > InteractableObject.

    • Set Interaction Type to Tap_ChangeValue.

    • Set Numerical Value to 0.

    • Drag its child ValueText into the Value Text Display slot.

    • Set Value Increment to 1, Value Min to 0, Value Max to 9.

    • Crucially: Uncheck NumberBlock in the Hierarchy to hide it initially.

  3. Create Comparison Objects (Puzzle 13):

    • Right-click Canvas > UI > Image. Rename it Num1.

    • Set its Rect Transform (e.g., X: -200, Y: 0, Width/Height: 150). Choose a light gray.

    • Add Component > InteractableObject.

    • Set Interaction Type to Compare_Value.

    • Set Numerical Value to 7.

    • Right-click Num1 > UI > Text - TextMeshPro. Rename it ValueText.

    • Set ValueText's Rect Transform (e.g., X: 0, Y: 0, Width/Height: 150). Set its text to "7", font size to 80, alignment to center.

    • Drag ValueText (child of Num1) into Value Text Display on Num1's InteractableObject.

    • Duplicate Num1 (Ctrl+D), rename to Num2. Position X: 0, Y: 0. Change its Numerical Value to 3 and update its child ValueText to "3".

    • Duplicate Num1 again, rename to Num3. Position X: 200, Y: 0. Change its Numerical Value to 9 and update its child ValueText to "9".

    • Crucially: Uncheck Num1Num2Num3 in Hierarchy to hide them initially.

  4. Create Draggable Text (Puzzle 14):

    • Temporarily disable your main QuestionText GameObject in the Hierarchy for this puzzle.

    • Right-click Canvas > UI > Text - TextMeshPro. Rename it QuestionPart1.

    • Set its Rect Transform (e.g., X: -200, Y: 400, Width/Height: 400, 100). Text: "This".

    • Add Component > InteractableTextCan Be DraggedUNCHECKED.

    • Right-click Canvas > UI > Text - TextMeshPro. Rename it DragMeWord.

    • Set its Rect Transform (e.g., X: 0, Y: 400, Width/Height: 200, 100). Text: "not".

    • Add Component > InteractableTextCan Be DraggedCHECKEDTarget TypeTextSegment.

    • Right-click Canvas > UI > Text - TextMeshPro. Rename it QuestionPart2.

    • Set its Rect Transform (e.g., X: 200, Y: 400, Width/Height: 400, 100). Text: "a trick!".

    • Add Component > InteractableTextCan Be DraggedUNCHECKED.

    • Crucially: Uncheck QuestionPart1DragMeWordQuestionPart2 in Hierarchy.

  5. Update 

    • Select _GameManager in the Hierarchy.

    • In the Inspector, under GameManager (Script):

      • All Scene Interactable Objects: Increase Size to include NumberBlockNum1Num2Num3. Drag all 23 InteractableObjects into their slots.

      • All Scene Interactable Texts: Set Size to 3. Drag QuestionPart1DragMeWordQuestionPart2 into their slots.

  6. Create New 

    • In your _Scripts folder, Right-click > Create > TrickyLogicPuzzle > Puzzle Data.

    • Puzzle 12: Change Value

      • Name: Puzzle12_ChangeValue.

      • Puzzle Name: Get to Five

      • Question: Tap the block until it's 5!

      • Hint: Count carefully.

      • Interactable Objects: Size 1. Drag NumberBlock into Element 0.

      • Numerical PuzzleComparison TypeNoneCorrect Numerical Value: 5.

    • Puzzle 13: Smallest Number

      • Name: Puzzle13_SmallestNumber.

      • Puzzle Name: Which is Smallest?

      • Question: Tap the smallest number!

      • Hint: It's not always obvious.

      • Interactable Objects: Size 3. Drag Num1Num2Num3 into the elements.

      • Numerical PuzzleComparison TypeSmallestCorrect Numerical Value: 3.

    • Puzzle 14: Drag Text out of Question

      • Name: Puzzle14_DragText.

      • Puzzle Name: Remove the "not"

      • Question: This is not a trick!

      • Hint: What if it wasn't there?

      • Interactable Objects: Size 0.

      • Interactable Texts: Size 3. Drag QuestionPart1DragMeWordQuestionPart2 into the elements.

      • Numerical PuzzleComparison TypeNoneCorrect Numerical Value: 0.

  7. Update 

    • Select _GameManager.

    • Set Size to 14.

    • Drag Puzzle12_ChangeValue into Element 11.

    • Drag Puzzle13_SmallestNumber into Element 12.

    • Drag Puzzle14_DragText into Element 13.

  8. Save Your Scene: File > Save (or Ctrl+S / Cmd+S).

Test Your New Puzzles!

Run your game in Unity. Play through your existing puzzles until you reach the new ones:

  • Puzzle 12 (Get to Five): Tap the block until it displays '5'.

  • Puzzle 13 (Which is Smallest?): Tap the number '3'.

  • Puzzle 14 (Remove the "not"): Drag the word "not" away from the question.

If everything is configured correctly, you'll hear appropriate sounds and see "CORRECT!" feedback as you solve them.

Conclusion

By following this step-by-step guide on how to implement number and text manipulation, you've unlocked a whole new dimension of puzzle design in Unity. From subtle numerical challenges to direct interaction with the narrative, your players will appreciate the increased engagement. Experiment with these tools, combine them with existing mechanics, and watch your creative puzzle ideas come to life

Comments

Popular posts from this blog

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

How to Create a Brain Test–Style Puzzle Game in Unity (Step-by-Step Guide for Beginners)

Master Drag & Drop in Unity: Build Engaging Puzzle Games (Step-by-Step Tutorial)