Draw-to-solve puzzles are one of the most reliably engaging formats in mobile gaming. The concept is simple to explain in one sentence — draw a line to protect or guide something — yet the implementation touches almost every system in Unity: input handling, 2D physics, procedural mesh generation, and level design tooling. Games in this genre (protecting a character from hazards by sketching barriers, guiding a ball through a maze you draw yourself, or fencing off danger zones) share a common technical backbone, and that backbone is worth understanding in depth if you're building anything in this space.
This article breaks down how draw-to-solve mechanics actually work under the hood in Unity: converting a finger swipe into a physical object, managing limited "ink" as a resource, structuring levels so difficulty scales predictably, and avoiding the performance traps that show up once you have dozens of hand-drawn colliders on screen at once.
1. From Touch Input to a Physical Line
The first technical challenge is converting a raw touch gesture into something the physics engine can collide with. This happens in three stages: capturing input points, rendering the line, and generating a collider that matches it.
Capturing input points
You don't want to record every single touch event — on a modern phone that can be hundreds of points per second, most of which are redundant. Instead, sample points at a minimum distance threshold:
if (Vector2.Distance(lastPoint, currentPoint) > minPointDistance)
{
linePoints.Add(currentPoint);
lastPoint = currentPoint;
}
This keeps your line smooth without flooding your point list with near-duplicate positions, which matters both for rendering performance and for the next step.
Rendering the line
Unity's LineRenderer is the natural fit here. It takes your sampled points directly and handles width, corner smoothing, and material rendering without you needing to build custom meshes for the visual layer. The main tuning knobs are numCornerVertices and numCapVertices, which control how smooth curves and line ends look — worth bumping up slightly for thin, fast-drawn lines that would otherwise look jagged.
Generating a matching collider
This is the part that trips people up. LineRenderer is purely visual — it doesn't collide with anything. You need to generate an EdgeCollider2D (or a chain of small BoxCollider2D segments) that follows the same points:
EdgeCollider2D edgeCollider = drawnObject.AddComponent<EdgeCollider2D>();
edgeCollider.points = linePoints.ToArray();
EdgeCollider2D is generally the better choice over stitching together box colliders — it's lighter weight and handles arbitrary curved paths natively. The one caveat is that edge colliders are one-sided in terms of physics behavior in some configurations, so test collision response from both approach directions during development, not just the one you happen to test with first.
2. Treating Ink as a Resource, Not Just a Cosmetic Meter
Most games in this genre limit how much a player can draw, and that constraint is what turns "draw a line" into an actual puzzle rather than a trivial task. The naive approach is to track ink as "number of points drawn," but that ties your resource economy directly to your sampling rate, which is a mistake — change your point-distance threshold later and you silently rebalance every level.
The more robust approach is to track ink by cumulative line length:
float segmentLength = Vector2.Distance(previousPoint, newPoint);
currentInkUsed += segmentLength;
if (currentInkUsed >= maxInkAllowed)
{
StopDrawing();
}
This decouples your resource budget from implementation details like sampling rate and screen resolution, which matters a lot once you're designing dozens of levels and need ink budgets to feel consistent across different device DPIs.
It's also worth exposing ink remaining as a normalized value (0 to 1) rather than a raw float, since your UI, difficulty curve, and any "undo" or "erase" mechanics will all want to reference it that way.
3. Structuring Hazards and Win Conditions Independently of Drawing
A recurring architectural mistake — similar to what shows up in physics-based sports and board games — is coupling win/loss detection directly to the drawing system. It's tempting to check "did the hazard's path get blocked" inside the same script that handles touch input. This works until you add a second hazard type, at which point the drawing script becomes a dumping ground for unrelated game logic.
A cleaner separation:
- Drawing system — only responsible for capturing input, rendering the line, generating the collider, and tracking ink. It doesn't know or care what the line is protecting against.
- Hazard system — each hazard (a bee, a falling object, a rolling ball) is its own component with its own movement and damage logic. It reacts to whatever colliders happen to be in its way, drawn or not.
- Objective system — a separate controller watching win/loss state (e.g., "target survived for N seconds," "target reached the goal zone"). It listens to events from the hazard system rather than polling collision state directly.
This separation pays off the moment you want to introduce a new hazard type or a new win condition — you're adding a new component, not modifying a monolithic controller that already handles five unrelated responsibilities.
4. Designing a Difficulty Curve That Actually Escalates
Puzzle games live or die on their difficulty curve, and "escalating" doesn't just mean adding more hazards. There are several independent difficulty levers, and mixing them deliberately produces a much more interesting curve than cranking all of them up together:
- Ink scarcity — reducing the ink budget relative to the space that needs covering forces more efficient, minimal solutions.
- Hazard timing — a hazard that appears immediately is easier to plan around than one that spawns mid-level, forcing players to react rather than pre-plan.
- Spatial complexity — more open space with multiple valid solution paths early on; tighter, more constrained geometry later that narrows the solution space.
- Time pressure — a soft timer or a hazard that accelerates over time adds pressure without changing the core puzzle logic at all.
A practical technique is to build a simple internal scoring system per level — something like (hazard count × timing complexity) + (spatial complexity / ink budget) — and use it to sanity-check that your level order actually escalates in the way you intend, rather than relying purely on gut feel. It's easy to accidentally place a genuinely harder level before an easier one just because it "felt" like the next step during design.
5. Level Data as Configuration, Not Code
Because this genre lives or dies on having a large number of levels, hardcoding hazard positions and ink budgets in scene files scales badly. A ScriptableObject-based level definition is a far better fit:
[CreateAssetMenu(fileName = "LevelData", menuName = "Puzzle/LevelData")]
public class LevelData : ScriptableObject
{
public float maxInk;
public float surviveDuration;
public HazardConfig[] hazards;
public Vector2 targetStartPosition;
}
This has a few concrete benefits beyond just tidiness:
- Designers (or you, wearing a designer hat) can tune values without touching scene hierarchies.
- Levels can be loaded dynamically by index, which makes A/B testing difficulty curves trivial.
- It's straightforward to build a lightweight in-editor level browser that instantiates any
LevelDataasset into a test scene, dramatically speeding up iteration.
If you eventually want server-driven levels (for live-ops style content updates without an app store release), the ScriptableObject structure also maps cleanly onto a JSON schema, since the fields are simple serializable types.
6. Performance Considerations Specific to This Genre
Draw-to-solve games look simple, but a few performance issues show up reliably once you're testing on real mid-range Android hardware rather than an editor or flagship device:
Collider count creep. If a player draws several long, winding lines across a level, and each becomes its own EdgeCollider2D, you can end up with more colliders active simultaneously than you'd expect. Since these are typically static once drawn, marking them appropriately and avoiding unnecessary Rigidbody2D components on drawn objects (a kinematic or static collider is usually sufficient) keeps physics step costs down.
Line renderer vertex count. Long, detailed lines with high corner vertex counts add up in render cost, especially with multiple lines active at once. A simple mitigation is to apply light point simplification (removing points that don't meaningfully change the line's direction) before finalizing the drawn line, rather than keeping every raw sampled point.
Garbage collection from per-frame allocations. It's common to see List<Vector2> allocations happening every frame during active drawing if you're not careful about reusing collections. Pre-allocating your point list with a reasonable capacity and clearing rather than reallocating between draws avoids unnecessary GC pressure, which matters more on this genre's target audience of budget and mid-range devices than it might for a higher-spec game.
Touch input smoothing cost. If you're applying smoothing algorithms (like Catmull-Rom interpolation) to raw touch points for a nicer-looking line, do it once when finalizing the line rather than recalculating the entire smoothed path on every new point added — that's an easy accidental O(n²) cost as lines get longer.
7. Testing Solvability
One issue specific to physics-based puzzle design that's easy to overlook: because your solution space is player-drawn geometry rather than a fixed set of moves, it's possible to accidentally design a level that's unsolvable with the ink budget you've assigned, especially after later tweaking hazard speed or spawn timing. A lightweight internal tool — even something as simple as an automated "bot" that tries a handful of heuristic drawing strategies and reports whether any of them succeed — can catch these regressions before they ship, rather than relying entirely on manual playtesting to catch a broken level.
8. Handling Undo, Erase, and Multi-Touch Edge Cases
Once a drawing mechanic ships, players immediately start testing its edges — drawing with two fingers at once, starting a new line before finishing the last one, or expecting to erase a mistake without losing their entire ink budget. A few patterns handle these cleanly:
Single active stroke enforcement. Most implementations restrict input to one active touch for drawing, ignoring secondary touch IDs rather than trying to support simultaneous multi-line drawing. This avoids a whole class of state-management bugs where two EdgeCollider2D objects are being built concurrently from interleaved touch events.
if (Input.touchCount > 0 && activeTouchId == -1)
{
Touch touch = Input.GetTouch(0);
activeTouchId = touch.fingerId;
}
Undo as object removal, not ink refund by formula. Rather than recalculating ink mathematically when a line is erased, it's simpler and less bug-prone to store the ink cost alongside each drawn line object when it's created, then simply refund that stored value and destroy the object on undo. This avoids drift between your ink tracking and what's actually on screen, which is a common source of "ink went negative" bugs.
Debounced erase gestures. If erasing is triggered by a gesture like a tap-and-hold on an existing line, add a short delay before triggering removal so that a quick accidental tap during normal drawing doesn't delete a line the player wanted to keep.
9. Cross-Device Input Consistency
Draw-to-solve games are unusually sensitive to input differences between devices, more so than many other mobile genres, because the entire mechanic is built on precise gesture capture. A few things worth testing explicitly rather than assuming they'll "just work":
-
DPI and screen size variance. A
minPointDistancethreshold tuned in editor on a desktop-simulated screen often feels too sparse or too dense on an actual device. Testing on both a small-screen budget phone and a larger flagship tends to surface this quickly. - Touch pressure and palm rejection. Some devices report phantom touch points from a resting palm during drawing, particularly on larger tablets. Filtering touches by a minimum distance from the primary touch, or simply capping to a single tracked touch ID, avoids most of this.
-
Editor mouse input vs. device touch input. It's easy to build and test an entire drawing system using
Input.mousePositionin editor and only discover touch-specific quirks (multi-touch noise, different pressure curves) once you build to an actual device. Testing on hardware early, even with placeholder art, avoids late-stage surprises.
Closing Thoughts
Draw-to-solve puzzle mechanics look deceptively simple from a player's perspective, but a solid implementation touches input sampling, physics collider generation, resource management, and level configuration — each of which benefits from being kept as its own clearly bounded system rather than tangled together. Getting the ink-as-resource model right, decoupling hazards from the drawing system itself, and treating levels as data rather than hardcoded scenes are the three decisions that will save the most rework as your level count grows from a handful of prototypes to a full game.
These same principles — separating input capture from physics response, and treating game content as configurable data rather than code — apply broadly across mobile genres, including runner and arcade games where core loop tuning and difficulty pacing follow a similar pattern. This breakdown of building an endless roller game in Unity covers that side of the equation, including core loop structure and monetization pacing: Building an Endless Roller Game in Unity.
For a working reference implementation of the draw-to-protect mechanic covered in this article, a complete Unity puzzle rescue project is available here: Save The Dogs Unity Game Source Code.

Top comments (0)