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:
Create the Script: In your Unity project, navigate to your _Scripts folder. Right-click > Create > C# Script and name it InteractableText.
Add the Code: Open InteractableText.cs and paste the following code. This script will handle the core drag-and-drop functionality for text elements.
using UnityEngine;
using TMPro;
using UnityEngine.EventSystems;
using DG.Tweening;
public enum InteractableTargetType
{
None,
ObjectTarget,
QuestionText,
HintButton,
CoverSpot,
InvisibleTrigger,
TextSegment
}
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();
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;
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;
}
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:
Modify
Open InteractableObject.cs.
Add new Enums: Under InteractableObjectType, add:
Compare_Value,
Tap_ChangeValue,
Add Numerical Fields: Under a new [Header("Value/Text Specifics (NEW)")] section, add:
[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():
private void Awake()
{
if (valueTextDisplay != null)
{
initialNumericalValue = numericalValue;
UpdateValueDisplay();
}
}
Modify Handle Tap_ChangeValue:
public void OnPointerClick(PointerEventData eventData)
{
if (interactionType == InteractableObjectType.Tap_ChangeValue)
{
ChangeValue();
}
GameManager.Instance.HandleTapInteraction(this);
}
Modify Allow Compare_Value objects to be dragged:
if (interactionType != InteractableObjectType.Drag_ToTarget &&
interactionType != InteractableObjectType.Compare_Value) return;
Add
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;
Update Reset numerical value:
public void ResetVisuals()
{
if (valueTextDisplay != null)
{
numericalValue = initialNumericalValue;
UpdateValueDisplay();
}
}
Save InteractableObject.cs.
Modify
Open GameManager.cs.
Add
[SerializeField] private InteractableText[] allSceneInteractableTexts;
Add
[SerializeField] private AudioClip numberChangeSound;
Update Add cases for Tap_ChangeValue and Compare_Value:
public void HandleTapInteraction(InteractableObject tappedObject)
{
switch (tappedObject.GetInteractionType())
{
case InteractableObjectType.Tap_ChangeValue:
PlaySound(numberChangeSound);
if (CheckNumericalValueSolution(tappedObject)) { isCorrect = true; } else { return; }
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;
}
ProcessInteractionResult(isCorrect, tappedObject);
}
Add
public void HandleTextDragInteraction(InteractableText draggedText, bool isCorrectDrag)
{
if (feedbackText.gameObject.activeSelf) return;
PlaySound(uiInteractionSound);
HideHint();
bool isCorrect = isCorrectDrag &&
allPuzzles[currentPuzzleIndex].interactableTexts.Contains(draggedText) &&
draggedText.GetTargetType() == InteractableTargetType.TextSegment;
ProcessInteractionResult(isCorrect, null);
if (isCorrect) { draggedText.gameObject.SetActive(false); }
}
Add
public enum ComparisonType { None, Smallest, Largest }
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
{
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():
private IEnumerator HideFeedbackAfterDelay(bool loadNext)
{
ResetAllInteractables();
ResetAllInteractableTexts();
}
private IEnumerator ResetPuzzleAfterDelay()
{
ResetAllInteractables();
ResetAllInteractableTexts();
}
Add
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.
private void LoadPuzzle(int index)
{
foreach (InteractableText txt in allSceneInteractableTexts) { if (txt != null) { txt.gameObject.SetActive(false); } }
foreach (InteractableText txt in puzzleToLoad.interactableTexts)
{
if (txt != null)
{
txt.gameObject.SetActive(true);
txt.ResetVisuals();
}
}
}
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:
Modify
Open PuzzleData.cs.
Add
public InteractableText[] interactableTexts;
Add Numerical Puzzle Fields:
[Header("Numerical Puzzle (NEW)")]
public GameManager.ComparisonType comparisonType = GameManager.ComparisonType.None;
public int correctNumericalValue = 0;
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:
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.
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.
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 Num1, Num2, Num3 in Hierarchy to hide them initially.
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 > InteractableText. Can Be Dragged: UNCHECKED.
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 > InteractableText. Can Be Dragged: CHECKED. Target Type: TextSegment.
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 > InteractableText. Can Be Dragged: UNCHECKED.
Crucially: Uncheck QuestionPart1, DragMeWord, QuestionPart2 in Hierarchy.
Update
Select _GameManager in the Hierarchy.
In the Inspector, under GameManager (Script):
All Scene Interactable Objects: Increase Size to include NumberBlock, Num1, Num2, Num3. Drag all 23 InteractableObjects into their slots.
All Scene Interactable Texts: Set Size to 3. Drag QuestionPart1, DragMeWord, QuestionPart2 into their slots.
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 Puzzle: Comparison Type: None, Correct 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 Num1, Num2, Num3 into the elements.
Numerical Puzzle: Comparison Type: Smallest, Correct 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 QuestionPart1, DragMeWord, QuestionPart2 into the elements.
Numerical Puzzle: Comparison Type: None, Correct Numerical Value: 0.
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.
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
Post a Comment