You’re about to drop a single, performance-safe script into a PlayMobile.online scene that builds the whole core loop: tile pooling and recycling, distance scoring, and a deterministic difficulty ramp that runs on phones without GC spikes.
Create an empty RunnerCore GameObject, add the provided script, and assign Player Transform, a UI label, a tile prefab, an obstacle prefab array, and optional pooled roots.
Working, paste-first C# snippet:
using UnityEngine;
using UnityEngine.UI;
public class RunnerCore : MonoBehaviour {
public Transform player;
public Text scoreLabel;
public GameObject tilePrefab;
public GameObject[] obstacles;
public int poolSize=8;
Transform[] pool;
int head=0;
float baseSpeed=6f; AnimationCurve speedCurve=AnimationCurve.Linear(0,1,60,2);
int lastScore=0; float uiThrottle=0.1f;
void Start(){
pool=new Transform[poolSize];
for(int i=0;i
This snippet avoids two big beginner mistakes: Instantiate/Destroy per tile that causes stutter and random per-frame spawning that breaks determinism.
If your player jitters or stops, check collider seams and too many colliders first—these often cause physics snagging, as forum reports showed. Next you’ll wire the scene for phones, tighten colliders, and profile on-device to confirm no GC spikes during a multi-minute run.
Drop-in Core Loop Script: Tile Spawning, Scoring, and Difficulty in One Pass
Get the whole tile pool, scoring, and speed ramp working from one compact script you paste into a GameObject. You will create a RunnerCore object and wire four inspector fields so the loop runs immediately.
- Create a GameObject named
RunnerCoreand addRunnerCore.cs. - Under RunnerCore add two empty children:
TilesRootandObstaclesRootto keep pooled objects organized. - Assign references in this order: Player, UI Text, tile prefab, obstacle prefabs. Then run once before tuning probabilities.
What the single script does in one pass: it pre-warms a fixed pool of tiles and objects, repositions old segments behind the player, updates a distance score, and samples a speed curve to ramp difficulty. This keeps state in one inspector so you can trace spawn, recycle, score, and ramp logic without cross-script indirection.
| Field | Recommended | Reason |
|---|---|---|
| Tile length (m) | 10 | Predictable recycle distance and camera fit |
| Tiles on screen | 6–10 | Start low to avoid pop-in and extra colliders |
| Recycle buffer | 5 m | Avoid visible repositioning |
| Obstacle spawn chance | 10–30% | Control difficulty without extra draw calls |
| Speed ramp | Start 6 → Max 20; curve | Deterministic difficulty scaling |
Quick sanity checks you can run in under a minute: confirm active tile count remains constant, verify obstacle count does not grow, and profile to see 0B GC alloc per frame during steady play. Common beginner mistakes to avoid: allocating lists in Update, calling GetComponentsInChildren at runtime, or updating UI text every frame when score hasn’t changed.
Project Setup That Won’t Fight You on Mobile
Start with a lean project layout so your scene stays predictable on devices. A clear hierarchy reduces Find() calls, stops accidental duplicates after reloads, and makes pooling cleanup trivial when restarting a level.
- Player
- MainCamera
- RunnerCore
- Canvas (Screen Space – Overlay)
- EventSystem
- TilesRoot, ObstaclesRoot, optional VFXRoot
Movement baseline: keep forward motion constant and handle lane changes with a single MoveLane(int dir) method. Use kinematic-style snapping (MoveTowards or SmoothDamp) or a Rigidbody with interpolation. Do not mix Transform.Translate with a non-kinematic Rigidbody; that causes jitter.
| Issue | Quick fix |
|---|---|
| FixedUpdate vs Update mismatch | Drive physics in FixedUpdate |
| Collider seams | Align pivots and tile edges |
| Default drag/constraints | Reset or tune Rigidbody settings |
Pick target frame rate intentionally (60 for mid/high, 30 for low-end) and avoid cranking Fixed Timestep without measuring. These settings keep physics stable in your unity project and protect battery life while running.
Unity endless runner tutorial, procedural spawning mobile: Building Tiles as Reusable Chunks
Make each tile a predictable, testable chunk so your track never produces surprise gaps or physics snags. Define strict prefab rules up front and test them in-editor before playtesting on device.
Tile prefab rules
Every tile should be exactly N units long. Set the pivot at the tile start (z=0). The end must sit at z=N with no mystery offset.
Validate by dropping two instances end-to-end. Check for gaps, overlaps, and aligned lane markers.
Spawn points as child transforms
Create child transforms like SpawnPoints/LaneLeft_01, LaneMid_01, LaneRight_01. Iterate those transforms instead of calculating random XYZ positions at runtime.
- Less per-frame math
- Fewer overlaps
- Easy visual review inside the prefab
Chunk strategy and optional tooling
Keep only ~5–10 segments alive and recycle them to reduce collider count and seam issues. This approach is a common forum best practice for stable physics.
If you need authored variation, consider Dreamteck Forever from the Asset Store for segment snapping and level streaming. Note: many seam problems come from mesh bounds or pivot misalignment, so align edges to bounds before using such tools.
| Check | Action | Why |
|---|---|---|
| End-to-end placement | Drop two tiles at z=0 and z=N | Confirms no gap or overlap |
| Spawn point alignment | Inspect child transforms in prefab | Prevents runtime lane offsets |
| Collider count | Limit active segments to 5–10 | Reduces snagging and CPU cost |
Object Pooling That Actually Stops Your FPS Drop
If your frame rate drops after a minute, the object lifecycle is the first place to inspect. Repeated Instantiate/Destroy builds garbage and forces GC runs on the main thread. That creates short but painful hitches on phones.
Prewarm pools in Start/Awake: instantiate tiles and obstacles once, disable them, and parent them under a pool root. This prevents scene reload duplicates and avoids costly creation during play.
Sizing and recycling
Use simple sizing rules: tiles pool = tiles on screen + 2 buffer. Obstacles pool = ceil(tiles on screen * spawn points per tile * max spawn probability).
When the player crosses a recycle threshold, move the oldest tile to spawnZ and reuse its obstacle slots. Reset transforms, colliders, visual state, and animation flags before enabling an object.
| Item | Rule | Why |
|---|---|---|
| Tiles pool | On-screen + 2 | Keeps buffer to hide repositioning |
| Obstacles pool | RoundUp(spawn slots * prob) | Avoids runtime allocations |
| Prewarm timing | Start/Awake | Single allocation window |
Common mistakes: letting pooled lists grow, or forgetting to reset colliders and state. Track ActiveTiles and ActiveObstacles in a dev UI so counts stay flat. That one check answers a lot of performance questions during testing.
Obstacles, Gates, and Reliable Collisions
Decide obstacle placement when a tile is recycled so collisions and triggers behave predictably. Set configuration once and let the tile run; this avoids per-frame random rolls and non-repeatable runs in an endless runner.
Deterministic obstacle placement
Assign a weighted chance per spawn point during tile setup. Each spawn point stores allowed types and a probability. This keeps difficulty stable and minimizes runtime work.
Trigger and collider setup
Set isTrigger or enable/disable colliders at spawn time, not inside unrelated callbacks like material-change handlers. That removes jitter from late physics state changes.
Pitfalls and mitigations
Many small box colliders create seams the physics engine can catch, and that causes the player to halt unexpectedly. Use a single simple collider per tile where possible and align seams to avoid micro-gaps.
| Issue | Fix | Why |
|---|---|---|
| Collider stacks | Merge or simplify colliders | Reduces snagging and CPU cost |
| Late trigger toggles | Configure at spawn | Keeps physics deterministic |
| Complex gate types | Use one collider per gate | Easier contact resolution |
- On phones, fewer colliders save CPU and battery.
- Preconfigure objects in the editor and verify lanes visually.
Scoring System: Distance, Multipliers, and UI Without Battery Drain
Measure progress with whole numbers and throttle updates to save frame time. Use the player’s Z (or a traveled-distance accumulator if you shift origin) to derive an integer score. Integers format cleanly on small screens and avoid jitter from floating decimals.
Distance and throttled updates
Sample position each frame but update the on-screen text only when the displayed integer changes or at a fixed interval (for example, 10 times per second). This reduces costly text rebuilds and limits work per frame, saving battery and CPU time.
Multipliers without extra systems
Expose a simple multiplier hook on score calculation (score = baseDistance * combo). Keep the hook deterministic: change combo only at discrete events like perfect gates or streak thresholds so results are repeatable for testing.
- Use large touch targets for pause/retry buttons to aid usability.
- Anchor HUD elements away from notches and respect safe-area Insets in the scene.
- Avoid animating layout groups every frame; animate a single value if needed.
| Issue | Fix | Why |
|---|---|---|
| Text rebuilt every frame | Throttle updates | Reduces GC and frame hitches |
| Overlapping Text components | Consolidate into one label | Prevents extra draw and layout work |
| Frequent font/material swaps | Use static fonts and shared materials | Avoids render state churn |
Difficulty Scaling That Feels Fair (and Stays Deterministic)
Good pacing hands players a clear learning window before the challenge grows. That feeling drives replays and retention, a point GDC talks often summarize as the “core loop + difficulty ramp” approach to pacing. Keep increases predictable so you can balance and reproduce issues quickly.
Speed ramps vs. linear ramps
Think of speed as a reaction-time lever. Linear speed increases are simple but can steal reaction time too fast on small screens. An AnimationCurve-style ramp lets you give an early low-speed phase for learning, then a gradual skill phase that rewards mastery.
Spawn pressure without extra draw calls
Treat density as decision density, not unique geometry. Reuse pooled obstacle prefabs and toggle activation and placement to raise pressure. That increases challenge without new materials or extra draw calls, keeping performance steady.
Determinism and the feedback loop
Drive difficulty from distance milestones or a seeded process so runs are repeatable. Collect session feedback metrics—death distance histograms and fail reasons—and change one variable at a time. Small, measurable tweaks improve tuning and player experience.
| Knob | Effect | Balancing tip |
|---|---|---|
| Speed | Reduces reaction time | Use curve with gentle start |
| Spawn pressure | Increases decisions per second | Reuse pooled types, vary placement |
| Pacing | Retention driver | Tune via session metrics |
Mobile Performance Budget: Memory, Draw Calls, and Battery
Plan your performance budget around sustained smoothness, not short Editor demos. Aim for steady frame time after several minutes of play so thermal throttling and battery drain do not ruin the experience.
Below are focused, mobile-specific tactics you can apply to keep your game stable during long sessions. Use them together: rendering, memory, physics, and profiling interact on small devices.
Draw calls and rendering
Limit materials to one or two per tile and use texture atlases for repeated props. Favor GPU-instancing friendly meshes so repeating objects don’t multiply render state changes.
Memory and hidden allocations
Size pools to trade a bit of RAM for zero runtime allocations. Start conservative and increase pool sizes only if profiler traces show misses.
Avoid Update() allocations: no string concat per frame, no foreach on Unity collections, and no new arrays/lists. Confirm “GC Alloc” reads 0B during steady play.
Battery, thermals, and physics
Reduce expensive physics: fewer colliders, simpler rigidbodies, and stable FixedUpdate timing. Cap the frame rate so the device doesn’t hit sustained peak power and throttle CPU/GPU.
Profiling workflow
Reproduce a stutter after 2–5 minutes on a real device. Record a Profiler trace on-device, identify whether GC, CPU, physics, or rendering peaks cause the hitch, fix one bottleneck, and retest.
| Budget Item | Target | Quick Check | Action if exceeded |
|---|---|---|---|
| Materials per tile | 1–2 | Frame capture: draw calls | Atlas textures / merge materials |
| Pool sizing | On-screen + 2–4 buffer | Active object count steady | Increase pools, retest minutes-long run |
| GC Alloc | 0B steady-state | Profiler timeline | Eliminate per-frame allocations |
| Frame cap | 30–60 FPS (targeted) | Device thermal/clock logs | Lower cap or reduce physics load |
Unity Docs You’ll Use While Implementing This
When debugging late at night, the official docs are the fastest way back to a working scene. Bookmark the exact pages below so you can resolve object life-cycle, collision, and performance issues without guesswork.
Instantiate/Destroy and pooling guidance
Read Object.Instantiate and Object.Destroy to understand allocation costs. Frequent creation and destruction will show up as GC spikes.
- Key takeaway: use pooling to avoid per-frame allocations and reduce GC pressure.
Colliders, triggers, and rigidbody behavior
Check Collider.isTrigger, OnTriggerEnter, and OnCollisionEnter docs to learn when each event fires. Rigidbody presence and kinematic settings change which callback you get.
Profiler and GC tracking
Use the Profiler guide to record on-device traces, inspect GC Alloc, and isolate CPU spikes to scripts, physics, or rendering.
| Problem | Doc to open | Quick fix |
|---|---|---|
| Hitching / GC | Object.Instantiate / Destroy | Prewarm pools |
| Missing triggers | Collider & Rigidbody | Check kinematic and layer matrix |
| CPU spike | Profiler (on device) | Trace scripts vs physics |
Docs-first debugging list:
- If triggers don’t fire, verify Rigidbody setup and collision layers.
- If stutter appears, inspect GC Alloc and Instantiate counts in the profiler.
- If the player snags, simplify colliders and review contact points.
Conclusion
Conclude with focused checks that confirm stability, determinism, and UI readiness for your endless runner project.
You now have a production-shaped core loop: spawn → recycle → score → ramp, built so active counts stay flat and allocations do not creep over time. Pooling prevents stutter and a small segment count avoids collider overload that causes random stops.
Do this, not that: recycle tiles instead of destroying them, configure triggers at spawn time, and simplify or realign collider seams to stop abrupt halts.
One-sitting next steps: add swipe input, add a restart flow and basic obstacle set, then profile a 5-minute run on device before adding more art. Finalize UI: make pause/retry buttons thumb-friendly, respect safe-area notches, and throttle HUD updates.
Iterate using data: collect death distance and fail reasons, tune one variable at each level, and keep the process deterministic so you can reproduce questions from playtest feedback.
Written as a hands-on Unity mobile tutorial by George Jones for PlayMobile.online, focused on the parts that break first: pooling, collisions, and sustained performance.
