You will ship a playable, mobile-ready slice that includes movement, jump, touch buttons, camera follow, and coin pickups. This intro gives a working movement core so you can verify physics right away.
Follow the Unity Technologies “2D Beginner Adventure Game” learning path as a structured next step. Devin Curry’s series set the baseline: movement with touch buttons via Canvas UI and EventTrigger. You’ll avoid common beginner traps like HTML copy/paste errors that insert > and NullReferenceException from unassigned fields.
Test edge cases early — jump mashing, collider riding, and leaving platforms — or, as one tester joked, “My wife playing the simple platformer… broke it.” Also watch mobile limits: battery drain, excess draw calls, memory spikes, and varied screen sizes.
Drop this movement script onto your player to confirm Rigidbody2D behavior immediately:
using UnityEngine;
public class SimpleMove : MonoBehaviour
{
public float speed = 6f;
public float jumpForce = 8f;
Rigidbody2D rb;
void Awake() { rb = GetComponent<Rigidbody2D>; }
void Update()
{
float x = Input.GetAxisRaw("Horizontal");
rb.velocity = new Vector2(x * speed, rb.velocity.y);
if (Input.GetButtonDown("Jump")) rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
}
}
You’ll build a compact, testable scene with copy/paste-safe code, follow official Unity guidance, and avoid common early mistakes that break gameplay on phones.
Build a playable controller first: move + jump with Rigidbody2D
Start by making a tight, playable controller so you can test movement and jump before adding polish.
Create the Player GameObject
Make a GameObject with a SpriteRenderer, Rigidbody2D (Body Type = Dynamic), and a BoxCollider2D sized to the sprite. In the Rigidbody2D inspector, check Constraints → Freeze Rotation Z to prevent the player from spinning when colliding or when the camera moves.
Movement approach and FixedUpdate
Set horizontal motion by assigning rb.velocity for crisp, predictable strafing. Use AddForce with ForceMode2D.Impulse for jump so the impulse feels consistent. Run physics updates in FixedUpdate and read input in Update to keep timing stable.
Movement script and key lines
Include a ground-check raycast distance, a LayerMask for ground, and the velocity set vs AddForce lines. Watch for the HTML copy/paste bug: replace any GetComponent(); or the code will not compile.
| Inspector | Suggested Value | Why it matters |
|---|---|---|
| moveSpeed | 6 | Responsive horizontal feel on phones |
| jumpImpulse | 8 | Predictable jump height using impulse |
| Gravity Scale / Linear Drag | Gravity: 3, Drag: 0–0.2 | Tune fall speed; keep drag low to avoid sluggish control |
Ground-check checklist
- Ground objects have a Collider2D and are on the Ground layer.
- Player has Rigidbody2D and BoxCollider2D sized to artwork.
- LayerMask used by the raycast includes the Ground layer.
- Raycast origin sits above the floor surface, not inside geometry.
Keep the Rigidbody2D manual and collision callback docs open while wiring. The Rigidbody2D API and OnCollisionEnter2D / OnTriggerEnter2D pages will save time and prevent common setup mistakes like a missing component, wrong mask, or an undersized collider.
Project setup for a clean Unity 2D workflow on mobile
A tidy on-disk layout keeps work moving when you add levels, enemies, or UI. You want a small, repeatable system that reduces time hunting for prefabs and fixes mistakes fast on-device.
Folder layout that won’t turn into a mess after your second script
Create a minimal Assets/_Project tree at the start. Keep art, prefabs, scenes, and scripts in predictable places so teammates and devices see the same structure.
- Assets/_Project/Art/Sprites
- Assets/_Project/Prefabs
- Assets/_Project/Scripts
- Assets/_Project/Scenes
- Assets/_Project/UI
- Assets/_Project/Audio
This layout stops the “where did I put that prefab?” problem. It also makes source-control diffs easier and speeds up iterative testing on phones and tablets.
Import sprites and set Pixels Per Unit for consistent scale across devices
Match the sprite PPU to your tile grid so the character-to-platform ratio stays consistent across aspect ratios. Use camera size and UI anchors to handle different screens.
Set Filter Mode to Point for pixel art and Bilinear otherwise. Apply compression conservatively to avoid memory spikes on low-end Android devices.
| Setting | Suggested Value | Why it matters |
|---|---|---|
| Pixels Per Unit | 32 or 64 (match tiles) | Keeps sprite scale consistent across devices |
| Filter Mode | Point / Bilinear | Preserves crisp pixels or smooths textures |
| Compression | Low/Medium on phones | Reduces memory use on constrained hardware |
| Default Script Editor | Your IDE (set in Editor → Preferences) | Avoids “works on my machine” compile friction |
Keep your Project Settings and Editor preferences aligned across machines. Set the default script editor so double-clicking opens the right IDE and team builds remain reliable. This workflow is tuned for shipping fast to phones, not a desktop-only setup.
Scene basics: platforms, colliders, and a “don’t fall through the floor” checklist
Lay out your scene so collision shapes are predictable before you add art or enemies. A clean blockout saves hours of debugging later and makes raycasts trustworthy.
Quick blockout and collider tips
Duplicate ground tiles instead of scaling wildly. Use sliced sprites for long runs so collider shapes stay consistent. Keep each platform’s collider aligned to the sprite grid.
BoxCollider2D vs EdgeCollider2D
- BoxCollider2D: cheap, consistent, ideal for tiled floors and rectangles.
- EdgeCollider2D: good for slopes and irregular outlines but can create snag points if vertices misalign.
- Choose the simpler collider when possible to avoid unexpected catches and physics glitches.
Layer scheme and raycast hygiene
Use layers: Player, Ground, Collectible. Filter raycasts with a LayerMask so you don’t register ground on coins or UI colliders. This keeps grounded checks reliable for your platformer game.
| Issue | Fix | Why it matters |
|---|---|---|
| Tunneling | Use Continuous collision or increase fixed timestep | Prevents passing through thin platforms at low frame rates |
| Wall stick | Apply PhysicsMaterial2D with zero friction to player | Stops side contacts from halting horizontal movement |
| Missing collisions | Verify Z positions and overlapping colliders | Scene view can lie if objects are offset on Z |
Don’t forget to test like a chaos player: sprint off edges, land on corners, and slide along walls. These quick checks reveal most physics problems early in your game build.
Touch input you can ship: on-screen left/right buttons using Canvas + EventTrigger
Create touch input that survives different screen sizes: anchored buttons, a tiny controller object, and clean event wiring.
UI hierarchy and anchoring
Build this exact hierarchy: Canvas (Screen Space – Overlay) → TouchController (empty RectTransform, anchor: bottom stretch) → LeftButton and RightButton (Image components).
Anchor buttons to bottom corners with comfortable padding so thumbs hit them on 16:9 phones, elongated devices, and tablets. The container stretch preserves layout across resolutions.
EventTrigger wiring and movement API
Add an EventTrigger to each button and add Pointer Down and Pointer Up entries. Bind these to public methods on a small controller script that calls SetMove(-1), SetMove(0), or SetMove(1).
Keep UI logic out of physics. Let the controller script expose a clean API so later features like knockback or ice won’t require rewriting your input wiring. This helps when you start new gameplay coding.
Common device issues and fixes
- Ensure Canvas has a Graphic Raycaster.
- Include an EventSystem in the scene.
- Enable Raycast Target on button Images.
- Remove any full-screen invisible Images that block touches.
| Problem | Fix | Why |
|---|---|---|
| Works in Editor but not on device | Add Graphic Raycaster + EventSystem | Editor input can differ from touch raycasts on a device |
| NullReferenceException on EventTrigger binding | Assign player/controller reference in Inspector | EventTrigger calls fail when target fields are unassigned |
| Button doesn’t receive touches | Check Raycast Target and blocking UI | Invisible overlays can intercept pointer events |
Camera follow without motion sickness or wasted battery
Make the camera follow feel deliberate: smooth, simple, and predictable for the player. A calm camera preserves focus and reduces motion sickness on mobile devices.
Simple follow with dead zone
Use a lightweight script that interpolates the camera toward the player while honoring a dead zone. The dead zone prevents tiny player movements from causing jitter on small screens.
Keep the math cheap: a single Vector3.Lerp and a precomputed offset per frame avoids allocations and heavy CPU work.
Why parenting the camera is a named pitfall
Parenting the camera to the player looks like a quick fix, but when physics rotation or knockback occurs the camera inherits those transforms. That can spin the view and make the whole experience nauseating fast.
Instead, let the camera follow in script and freeze player rotation (Rigidbody2D → Freeze Rotation Z) so the character stays upright and the view remains stable.
Update frequency and battery note
Run camera visuals in LateUpdate so the view matches the latest physics frame without introducing jitter. Keep physics logic in FixedUpdate to stay deterministic.
- Avoid per-frame allocations and expensive distance checks.
- Limit complex smoothing to a single pass; every extra compute increases battery drain.
- Test on target devices to balance smoothness and power cost.
Collectibles: coins with trigger colliders, UI count, and zero-GC pickups
Design coin pickups so they trigger consistently, update your on-screen score, and reuse objects to avoid frame drops. Keep the prefab simple and predictable so testing takes less time and the player sees instant feedback.
Coin prefab and pickup plan
Make the coin with a SpriteRenderer and a CircleCollider2D set to Is Trigger. Tune a small pickup radius so touches feel fair on phones.
Tag coins “Collectible”. In your script use OnTriggerEnter2D to detect the collision, increment an int counter, and update a UI Text or TMP label only when the value changes. This avoids per-frame string allocations.
Trigger rule checklist and pooling
- Rule: both objects need Collider2D; at least one must have a Rigidbody2D; coin collider must be Is Trigger.
- Pool coins: pre-spawn, Disable on pickup, reuse on reset to avoid Instantiate/Destroy spikes and GC hitches on midrange Android devices.
- Update UI only on pickup to reduce string churn and GC time.
| Item | Recommended Setup | Why it matters |
|---|---|---|
| Coin prefab | SpriteRenderer + CircleCollider2D (Is Trigger) | Simple collision and visual clarity |
| Pickup code | OnTriggerEnter2D, tag check, increment counter, update UI | Reliable detection and zero-GC updates |
| Pooling | Pre-spawn pool, Disable/Enable reuse | Prevents frame drops from Instantiate/Destroy bursts |
| Debug checklist | Layers/collision matrix, tag spelling, UI ref assigned | Fixes most “trigger never fires” issues quickly |
Animations that won’t throw NullReferenceException
Make animation wiring a non-event: predictable parameters and safe references stop most runtime crashes. Use clear names and update values from your existing checks so the Animator stays in sync with the player state.
Animator parameters and update path
Define a minimal set: float Speed, bool Grounded, and trigger Jump. Set Speed from Mathf.Abs(rb.velocity.x). Set Grounded from your ground-check raycast. Fire Jump when you apply the jump impulse, not on button press alone.
Avoiding the classic “myAnim is null”
- Common causes: missing Animator on the GameObject, script not attached, or a public field never assigned in the Inspector.
- Fix pattern: add [RequireComponent(typeof(Animator))], call animator = GetComponent<Animator>() in Awake, and mark external refs [SerializeField] and assign them in Inspector.
- Debug tip: if the error line doesn’t match the tutorial, search for the null variable name and verify that field at runtime in the Inspector.
| Parameter | Type | Update source |
|---|---|---|
| Speed | float | Mathf.Abs(rb.velocity.x) in FixedUpdate |
| Grounded | bool | Ground-check raycast result |
| Jump | trigger | When AddForce impulse is applied |
Mobile performance pass for a 2D platformer: memory, draw calls, battery
Do a focused performance pass so your game runs smoothly across phones and tablets. Triage memory, draw calls, and CPU hot spots early. Test on a physical device, not just the Editor, because device hardware and thermals matter.
Sprite atlas and texture compression
Pack UI and in-game sprites into atlases to reduce texture swaps and draw calls. Use the engine’s sprite packing tools and an atlas per resolution bucket to keep VRAM low.
Apply compression per atlas. Avoid shipping everything uncompressed. Test on actual Android hardware for visible artifacts and memory use.
Reduce overdraw and UI cost
Large translucent buttons cost fill-rate. Tighten sprite shapes, trim alpha areas, and prefer opaque UI where possible.
Keep the UI hierarchy shallow and disable offscreen canvases. That lowers canvas rebuilds and GPU pixel work.
Physics and update frequency
Run only the raycasts you need. One grounded check is usually enough; avoid five per frame. Lower FixedUpdate if physics precision allows it.
Let static colliders stay static, and ensure dynamic bodies can sleep. Piles of awake rigidbodies will keep the CPU busy and heat the device.
- Profile builds on device with the Unity Profiler and CPU/GPU charts.
- Pool frequently spawned objects to avoid Instantiate/Destroy spikes.
- Cap frame rate if ultra-high FPS gives no gameplay benefit.
| Setting | Recommended Value | Why it matters |
|---|---|---|
| Sprite Atlas | Group by usage + resolution | Reduces texture swaps and lowers draw calls |
| Compression | Medium (test artifacts) | Saves VRAM while avoiding visible banding |
| FixedUpdate rate | Default → increase only if needed | Balances physics accuracy vs CPU cost |
| UI Alpha Usage | Minimize translucent pixels | Cuts overdraw and GPU fill work |
Device heat checklist: avoid unchecked high frame caps, heavy post-processing, many animators, and continuous allocations. Profile with a build on device; check CPU, GPU, and battery drain. Use your system preferences and Player settings to match target device limits before wide testing so your games behave well in the real world.
Player Settings and Build Settings for Android (US-targeted device reality)
Make a quick test build early to catch SDK, signing, or scene-order issues on a physical device. This saves time and gives real-world information about touch, aspect, and performance.
Build Settings: scenes-in-build and switching platform
Save your scene. Open File > Build Settings, add the scene to Scenes In Build, and make sure scene 0 is the entry point. Switch the platform to Android and do a small test build before adding more content.
Player Settings that affect feel
Set orientation (portrait or landscape) to match your control layout. Handle aspect and safe area by anchoring UI to device edges so notches and gesture bars do not hide buttons.
External Tools and quick triage
In Preferences > External Tools confirm the SDK/NDK/JDK paths. Common build failures start with mismatched API levels or missing SDK components. Change one variable at a time when you troubleshoot.
- Device test loop: install on a midrange phone, verify touch hitboxes, confirm stable frame times, and check package name/signing before sharing APKs.
| Setting | Recommended | Why |
|---|---|---|
| Orientation | Match control layout | Comfortable play and UI placement |
| Safe Area | Canvas anchors + RectMask | Prevents hidden buttons on US devices |
| External Tools | Correct SDK/JDK paths | Prevents “why won’t it build” errors |
Beginner mistakes you’ll hit fast (and how to fix them before they waste your night)
Most nights get wasted by simple wiring mistakes; hunting those first saves hours of frustration. This short guide lists the usual failures and a repeatable fix workflow so you can get back to adding features.
Quick checklist of the usual culprits
- Script not attached to the Player GameObject.
- Missing Rigidbody2D or colliders on expected objects.
- Empty LayerMask for ground checks or wrong layers on objects.
- UI buttons not wired, EventSystem or Graphic Raycaster missing.
- Public references left unassigned in the Inspector causing NullReferenceException.
Repeatable fix workflow
Open the Console, click the error, and jump to the exact line. Verify the referenced component exists on that GameObject or is assigned in the Inspector. If a field is null, mark it [SerializeField] or make it public and assign the correct reference in the Editor.
HTML copy/paste trap and device-only failures
If you copied code from a web tutorial and see > in your editor, replace it with > (the real greater-than sign). That artifact causes compile errors that look unrelated if you don’t know to check.
Works in Play but fails on a device? Check for missing EventSystem, GraphicRaycaster, or invisible UI layers blocking touches. Toggle suspect images off to find blockers quickly.
Edge-case tests — “my wife broke it”
Run these before you call the build done: hold direction while landing on a platform edge, mash jump at the lip, slide along a wall, and sprint off-screen to test camera bounds. Fixes mean the player stays responsive, won’t jitter, can’t jump repeatedly while grounded incorrectly, and won’t get stuck inside colliders.
unity 2d platformer mobile beginner mini project extensions you can add next
Focus on practical, low-cost features that expand play without bloating systems. Each extension below maps to a clear design pattern and avoids heavy CPU or GPU work.
Enemy patrol AI that turns at walls and edges
Use a forward raycast to detect walls and a downward raycast to detect missing ground, then flip the sprite and velocity. This mirrors Devin Curry’s Part 4 approach and keeps enemies predictable on phones.
Simple damage, death loop, and checkpoint respawn
Add a health int, a short hurt cooldown, and a checkpoint transform. On death, reset player position and health at the last checkpoint. This follows the Part 5a/5b direction so you build a full loop rather than isolated features.
Multiple levels with a clean scene-loading pattern
Keep one persistent UI scene (scoped managers + audio) and load per-level gameplay scenes additively. Alternatively use a bootstrap scene that loads UI then swaps level scenes to avoid duplicating managers and references.
- Minimize raycasts per frame; reuse pooled enemy objects to cut Instantiate/Destroy spikes.
- Profile extra enemies: more sprites and physics checks increase CPU/GPU and battery use over time.
- Practice step: follow Unity’s “2D Beginner Adventure Game” course to see these patterns applied in a structured tutorial.
Conclusion
Wrap up confidently: you now have a compact, playable game slice with movement, touch controls, camera follow that avoids spin, and clean collectible triggers.
Before you ship, test on a real device. Verify UI anchors across aspect ratios, run a development build to confirm no Console errors, and watch frame time for stability.
Double-check common pitfalls: frozen rotation to avoid camera spin, EventSystem and raycast setup, HTML copy/paste artifacts in code, and null references from unassigned fields. When in doubt, consult Rigidbody2D, Collider2D callbacks, and EventSystem docs for definitive information.
Keep performance basics in place: atlas + compression, reduce UI overdraw, pool frequent spawns, and trim physics checks to protect battery. If you have time, add enemy patrols and checkpoints, then turn this into a reusable template.

Game developer with over 10 years of professional experience specializing in the mobile sector. George’s journey began with a passion for indie development, leading him to contribute to several successful mobile titles, including the critically acclaimed puzzle-platformer ChronoShift and the top-down strategy game Pocket Empires.
