In this post you will see a tight, testable path from input to resolved cascades. The exact goal: you built a basic match-3 style board where one swipe swaps two neighboring pieces, then you validated matches and resolved cascades.
Start by verifying touch input with a minimal script. Paste-ready SwipeInput.cs below uses Input.touchCount and Input.GetTouch(0). It exposes minSwipePixels so you can tune feel without editing logic. Attach it and confirm the console prints start, end, and direction in under five minutes.
SwipeInput contract: it outputs a direction plus the start screen position. GridInput maps that to a board cell. The Board model applies the swap. The Presenter runs animations. The Resolver scans for matches and cascades.
You avoided common beginner mistakes: losing track of which finger moved, treating Touch like an instantiable object, and firing swipe actions multiple times per single gesture. Mobile constraints shaped choices: no per-frame allocations in hot paths, no LINQ in Update, pooled lists for scans, and fixed animation timing for stable battery use and frame time.
We started with 4-way directions to avoid diagonal ambiguity. Later sections show how to expand to 8-way if your design needs it. See Unity’s Touch documentation in the Manual for platform details.
// SwipeInput.cs - minimal single-touch verifier
using UnityEngine;
public class SwipeInput : MonoBehaviour {
public int minSwipePixels = 50;
Vector2 startPos;
bool tracking = false;
void Update() {
if (Input.touchCount == 0) return;
Touch t = Input.GetTouch(0);
if (t.phase == TouchPhase.Began) {
tracking = true;
startPos = t.position;
Debug.Log(\"Swipe start: \" + startPos);
} else if (tracking && (t.phase == TouchPhase.Moved || t.phase == TouchPhase.Ended)) {
Vector2 delta = t.position - startPos;
if (delta.magnitude >= minSwipePixels) {
tracking = false;
var dir = Mathf.Abs(delta.x) > Mathf.Abs(delta.y)
? (delta.x > 0 ? \"Right\" : \"Left\")
: (delta.y > 0 ? \"Up\" : \"Down\");
Debug.Log(\"Swipe end: \" + t.position + \" Dir: \" + dir);
}
if (t.phase == TouchPhase.Ended) tracking = false;
}
}
}
Quick setup (under 5 minutes):
1. Create an empty GameObject named “Input”.
2. Attach SwipeInput.cs.
3. Run on device or simulator and watch Console for start/end/direction logs.
Drop-in swipe detector you can run today (Touch.fingerId + GetTouch)
This section gives a compact, runnable touch detector that tracks one finger reliably and emits a single directional event. Paste the minimal script and run it on a device to confirm behavior quickly. The pattern follows Unity Scripting API polling in Update.
Minimal SwipeInput.cs you can paste
Keep the script small: record start position and start time on TouchPhase.Began, store activeFingerId = touch.fingerId, and only process the matching touch until Ended or Cancelled. Emit one event when distance exceeds threshold and the touch ends.
Tracking the right finger with Touch.fingerId
Store fingerId on Began and ignore other touches. This fixes the common bug where a second touch makes your first swipe stop or flip direction. Use Input.GetTouch(index) rather than iterating Input.touches to avoid temporary allocations.
Tap vs swipe thresholding and DPI scaling
Treat a gesture as a tap when distance < minSwipe and duration < maxTapTime. Otherwise treat it as a directional swipe using the dominant axis for 4-way normalization.
Concrete rule: minSwipePixels = Screen.dpi > 0 ? Screen.dpi * 0.2f : 80f. Use a short maxTapTime (like 0.2s) so taps and swipes are distinct across devices.
- Avoid foreach over Input.touches each frame — it can allocate.
- Never instantiate new Touch(); Touch is provided by the API.
- Do input polling in Update, not FixedUpdate, to match sampling timing.
- Keep Update cheap: a few Vector2 ops and branch checks; use Input.GetTouch(0) to reduce overhead.
For reference, keep Unity Scripting API docs open for Touch, Touch.fingerId, and Input.GetTouch(int) while you implement. This way you follow engine scripting patterns that avoid classic pitfalls.
Unity match puzzle mechanic, swipe gesture mobile game: how the input maps to a grid swap
Convert the raw vector from touch start to touch end into a single adjacent-cell action you can test. Do this by normalizing the screen delta and picking the dominant axis, then applying a deterministic neighbor rule so your board logic stays simple.
Converting screen direction into a neighbor selection
Compute dx and dy in screen space: endPos – startPos. Compare absolute values to pick horizontal or vertical.
- Rule (pseudo-code): if |dx| > |dy| then horizontal else vertical.
- Then neighbor = (x ± 1, y) for horizontal, or (x, y ± 1) for vertical.
Blocking diagonal ambiguity and choosing 4-way vs 8-way
Reject near-diagonals by requiring dominance: max(|dx|,|dy|) / (min(|dx|,|dy|) + 0.001) >= 1.5. Also require a DPI-scaled minimum distance so tiny flicks do not trigger a swap.
Use 4-way controls for classic match-3 boards; it reduces mis-swaps on large devices and fits thumb arcs. Only enable 8-way if diagonal swaps are central to your design, and raise dominance thresholds to avoid accidental moves.
Grid and board data model that won’t fight you later
Start by treating the board as pure data, not a visual scene you mutate directly. That keeps state deterministic and makes unit tests simple to write. It also limits per-frame work and GC churn on the engine.
The core model I used is minimal and explicit:
- int width, height
- int[,] pieceId — logical IDs for types
- CellFlags[,] — optional blockers or immovable markers
- Dictionary<Vector2Int, PieceView> — view pool mapping for assets
Name the separation: input (SwipeInput), simulation (Board + Resolver), presentation (animations/FX). With this split you can run swap and scan code without any sprites or canvases.
Preventing illegal swaps
Reject swaps when the neighbor is out of bounds, when either cell has a blocker flag, or when a piece type is immovable (for example, stone). This avoids many state problems and keeps the board valid between frames.
Deterministic swap API and debugging
Standardize TrySwap(a,b, out SwapResult result). The result struct contains pre/post IDs and a seeded RNG reference used for refills. Deterministic transitions let you cap cascade loops and avoid long unexpected resolves that cause stutter.
Finally, add a DumpGrid() helper that returns rows of IDs as text. Paste that into a bug report and you can reproduce the exact board state for replay and debugging.
Swipe-to-swap implementation in Unity (with animation hooks)
Implement a reliable swap flow that keeps your data authoritative and your visuals in sync.
Do not animate first. Commit the ID swap in the logical grid immediately, then play animations on the two PieceViews. This avoids desyncs and prevents a second input from seeing inconsistent state.
Order of operations I enforced:
- Lock input: set inputLocked = true before any change.
- Swap IDs in the data model right away.
- Play the swap animation on the two views.
- Run the match check and resolve loop.
- If no match, swap back in the model and animate the revert.
Lockout rules: use a single bool (inputLocked) that stays true until the entire resolve loop finishes. Do not clear it after the first animation. Expose the flag in your debug UI so testers can see when input is blocked.
Coroutine vs Update-driven tweens: coroutines are simple and fine when you avoid per-frame allocations and reuse WaitForSeconds objects. Update-driven tweens give tighter control and scale better during large cascades because they avoid many active enumerators.
Timing guideline for responsive feel: 0.08–0.12s for a swap. More than ~0.15s feels slow; shorter than ~0.06s can be too abrupt.
Swap-back is a common bug when visuals move before the model changes. Always swap the model first, then animate. Log swap start/end, coordinates, and whether a swap-back occurred so testers can give actionable feedback instead of vague reports about a bug.
Match finding from scratch (row/column scan)
You need a fast, low-allocation scanner to find horizontal and vertical runs. The goal was a simple two-pass approach that produced a compact list of clear coordinates you can feed to clearing and VFX code.
Efficient line scanning for runs of 3+ without allocating garbage
The algorithm ran two passes: rows first, then columns. For each line you counted a streak of identical piece IDs and skipped empty or blocked cells. When the streak length hit the threshold (3+), you recorded coordinates into a reused List<Vector2Int>.
Handling L/T shapes and overlapping matches in a single pass
While scanning you marked hits in a bool[,] matched grid. This merged overlaps naturally, so T and L shapes appeared as one combined set without extra unions. That kept logic simple and deterministic.
Returning a compact list of matched coordinates for clears and effects
The final return was a single compact list of matched coords plus an optional list of match groups (shape, length) if you needed to spawn special pieces. This list fed both clearing logic and presentation hooks directly, avoiding scene queries.
- Reused one List<Vector2Int> (or NativeList) across scans — no per-line temporary lists.
- Avoided LINQ, FindAll, and ToList() in hot paths to prevent GC spikes.
- Common beginner mistake: building temporary lists per row/col caused micro-stutters on mid-range devices.
| Approach | Allocations | Best for |
|---|---|---|
| Two-pass + reused lists | Minimal | Realtime on phones |
| Per-line temporary lists | High | Quick prototyping (not perf) |
| NativeList + bool grid | Low | High-performance builds |
Resolve loop: clear, collapse, refill, repeat
After a successful swap, the board enters a repeatable resolve loop that clears, collapses, refills, and rechecks until stable. Keep the loop deterministic and debuggable so you can reproduce cascades across devices and builds.
Typical flow:
- Find hits (scan rows and columns).
- Clear matched cells and queue visual effects.
- Collapse columns by compacting IDs downward.
- Refill empties with new pieces using a seeded RNG.
- Repeat until no hits or you hit the cascade cap.
Column compaction
For each column, walk bottom-to-top and write non-empty IDs to a writeIndex. After the pass, set cells above writeIndex to empty. This single pass beats per-cell fall checks and reduces branching and memory churn.
Refill rules and limits
When spawning, reject any random ID that would create an immediate three-in-a-row with the two left neighbors or the two below neighbors. Use a seeded RNG per board or level so cascades are reproducible for debugging.
Set a hard cap (for example, max 20 cascade iterations). If exceeded, rebuild or re-roll refills with a new seed to avoid hangs. On mobile, pace animations, pool FX, and watch memory to prevent long frame spikes on lower-end devices.
| Aspect | Value | Action on cap |
|---|---|---|
| Max cascades | 20 | Re-roll refill seed or rebuild board |
| RNG | Seeded per board | Reproducible cascades |
| Performance | Low alloc, pooled FX | Pace animations |
Touch input pitfalls pulled from real Unity Discussions threads
Real forum posts and developer Q&A revealed repeatable mistakes that cause frame spikes or flaky input. You should treat input as performance-sensitive state and follow tested patterns from community answers.
Why iterating Input.touches can allocate
Iterating the touches array or copying it each frame produces garbage. That causes small GC stalls that add up on low-end devices.
- Use Input.touchCount + Input.GetTouch(i) to poll without allocations.
- Keep a single record of active fingerId and reuse buffers for metrics.
The “I created a Touch class” mistake
Touch is a struct populated by the engine; creating new Touch() to “reset” it is unnecessary and wrong. Clear your own state (active fingerId, startPos, timers) instead of instantiating Touch objects.
Tap vs swipe conflict: immediate action then cancel
Doug_B’s option worked well in practice: fire the tap action on Began for instant feedback, then cancel that action if movement crosses the swipe threshold and perform the swipe instead.
- For tap-to-select plus swipe-to-swap: select on Began, then unselect and swap when swipe confirms.
- Always handle TouchPhase.Cancelled and Ended to clear state.
| Problem | Cause | Fix |
|---|---|---|
| GC spikes | Iterating Input.touches | Use GetTouch and reuse lists |
| State bugs | new Touch() attempts | Clear your stored state |
| Responsive feel | Waiting for Ended | Trigger tap immediately, cancel on move |
When you file or triage a bug report, capture fingerId, phase transitions, and distance-over-time. Link to the official unity engine scripting Touch and TouchPhase docs so readers can confirm Began/Moved/Stationary/Ended/Cancelled behavior and reproduce the issue to the end.
Mobile performance considerations specific to match puzzles
On handheld devices, small code choices drive big battery and frame-time differences. You should treat heavy work as explicit steps, not per-frame overhead.
Battery and frame time
Clearing, scanning, and cascade loops often spike CPU when many pieces move at once. They also stress the GPU during synchronized animations. Keep Update minimal and shift scans and RNG refill work into the resolve step to reduce steady frame pressure.
Memory and pooling
Avoid per-frame allocations. Pool piece GameObjects and common effects like clear particles. Reuse a single match list and a single matched-mask array per board to prevent GC spikes and reduce memory churn.
Draw calls and assets
Use sprite atlases so many sprites share one material and cut draw calls. Beware an always-rebuilding Canvas; mixing world sprites with UI can break batching. Test batching expectations on low-end GPUs before adding new assets.
Screen size, safe areas, and thresholds
Scale swipe thresholds by DPI or by a normalized screen fraction so input feels consistent across screen sizes. Respect safe areas and notch regions when laying out the board so touches never fall under OS UI on any device.
- Measure GC Alloc and Rendering in the Unity profiler to find hotspots.
- Watch overdraw when many pieces animate over bright backgrounds.
- Replace LINQ in hot paths with for-loops and reused arrays.
| Concern | Metric | Action |
|---|---|---|
| CPU spikes | Frame time | Move scans to resolve step |
| GC | GC Alloc | Pool objects and reuse lists |
| Render | Draw calls | Use atlases and shared materials |
Editor-friendly testing: mouse emulation + swipe debugging
Speed up iteration by letting the editor emulate touches so you can validate swaps and cascades without a build. Feed mouse presses into the same pipeline your runtime uses so behavior stays identical between editor and device.
The InputHelper pattern collects real touches and an optional fake touch from mouse phases. Implement a small script that produces the same Touch-like data your resolver expects. This lets you run the full resolve loop, exercise the refill RNG, and reproduce edge cases while you edit.
- Use an IInputSource abstraction with separate MouseInput and TouchInput implementations to avoid brittle reflection hacks.
- Avoid TouchCreator via reflection; field names change across engine versions and will break editors or CI.
- Keep the editor code behind #if UNITY_EDITOR or in a development tag so production builds stay clean.
On-screen debug overlays
Draw start/end points and the swipe vector over the board. Highlight the selected cell and display minSwipePixels, detected direction, and active fingerId.
| Tool | Benefit | When to use |
|---|---|---|
| Mouse emulation | Faster iteration, no build | Daily dev and prototyping |
| IInputSource | Stable API, testable | Team projects, CI |
| On-screen overlay | Immediate visual feedback | Threshold tuning and bug reports |
| Reflection TouchCreator | Quick hack | Only short-term experiments |
When you file a post or a forum report, attach overlay screenshots and exact threshold tags so others can reproduce the issue without guessing. That saves time and speeds up fixes.
Unity documentation and industry practices to follow while you build
Referencing official API pages early prevents guesswork when touch and screen behavior diverge. Keep the following scripting references open while you implement input and scaling: Input.GetTouch, Input.touchCount, Touch, TouchPhase, Touch.fingerId, and Screen.dpi / Screen.safeArea.
Adopt the separation practice: split Input, Simulation, and Presentation into distinct layers. Do not mutate board state inside render MonoBehaviours. Run simulation in plain C# classes so logic stays deterministic and unit-testable.
- Unit tests: swap legality, match detection, and resolve-loop termination.
- Play-mode tests: input lockouts and animation timing under load.
- Device checks: TouchPhase.Cancelled handling, multi-touch fingerId correctness, and safe area touch placement.
| Area | Docs to consult | Practice | Benefit |
|---|---|---|---|
| Input | Input.GetTouch / TouchPhase | Single active fingerId | Reproducible input |
| Scaling | Screen.dpi / Screen.safeArea | DPI-scaled thresholds | Consistent feel |
| Architecture | Engine scripting guides | Separate layers | Testable content |
| Project | API details | Folders: Input, Simulation, Presentation, Assets | Faster onboarding, clear categories |
Conclusion
Finish by locking the flow into a concise, testable pipeline that you can extend and ship.
Pipeline in one clear way: touch detection (fingerId + DPI thresholds) → grid neighbor selection (4-way) → deterministic model swap → animate views → run a fast scan → resolve loop (clear, collapse, refill) with a cascade cap.
Top don’ts to avoid: iterating Input.touches (allocations), not tracking fingerId, desyncing model and visuals, and letting a second swipe interrupt an active swap.
Mobile checklist: monitor GC Alloc during scans, pool piece GameObjects and FX, use atlases to keep draw calls low, and scale thresholds and layout for safe areas and different screen sizes. Tune buttons and controls consistently.
Next steps: add four-in-a-row specials, blocker rules, a timed hint scanner, and replay logging that records deterministic swaps. Document thresholds and the names you expose for buttons so your team can tune feel.
Ship advice: keep input a small module, make simulation authoritative, keep presentation replaceable, and publish the post and repo notes so the PlayMobile.online team and engine integrators can reproduce behaviour.
