πŸ”§ Tools Directory πŸ“° Blog πŸ‘οΈ Invisible AI 🧠 Micro-Habits
← Back to blog
βš”οΈ AI vs Human Workflow Series Battle 03 Β· Development

AI Coding
vs Junior Unity Dev
β€” Three Real XR Tasks

πŸ“… May 27, 2026 ⏱ 14 min read ✍️ Prabhu Kumar Dasari πŸ₯½ Unity Β· XR Β· AI Tools
Prabhu Kumar Dasari
Prabhu Kumar Dasari
Senior XR & AI Systems Developer Β· 13+ years building XR and AI systems in Unity
AI Coding vs Junior Developer
I've spent 13 years writing Unity code β€” from early AR prototypes to full shipped XR products. So when I decided to test AI coding tools in this battle, I chose tasks I actually work on. Not generic web forms. Three real Unity challenges: build an XR grab interaction with controller haptics, debug a broken coroutine scene-load crash, and write EditorTests for a spatial math utility. GitHub Copilot + Cursor on one side. Riya, a junior Unity developer with 18 months of professional experience, on the other. Everything timed. Every issue logged. I reviewed both outputs blind.

The Setup

The rules β€” identical for both sides
Unity version
Unity 6 LTS. XR Interaction Toolkit 3.x, AR Foundation 6.x, Input System package. Same starter project handed to both sides via Git.
AI tools
GitHub Copilot (inline + Chat) in Rider, and Cursor with Claude Sonnet. Both used as a working XR dev would use them β€” prompting, accepting, iterating.
Human dev
Riya, 18 months at an XR studio. Familiar with XR Interaction Toolkit and basic AR Foundation. No AI coding tools. Unity docs and forums allowed.
Scoring
Speed (time to working output), Correctness (no bugs / crashes in 10-min review), Code quality (readability, Unity best practices), and XR-specific nuance (haptics tuning, lifecycle awareness, frame-rate sensitivity).
Judge
Reviewed by me after both outputs were submitted β€” I did not know which was AI or human during scoring.

Task 01 β€” XR Grab Interaction with Haptic Feedback

Implement a custom GrabbableObject MonoBehaviour using XR Interaction Toolkit's XRGrabInteractable. On grab: trigger a short sharp haptic pulse (0.8 amplitude, 0.1s). On release: trigger a lighter fade pulse (0.3 amplitude, 0.08s). Highlight the object's material on hover. Handle the edge case where the controller loses tracking mid-grab without throwing a NullReferenceException.

Task 01 Β· XR Grab Interaction β€” XRI Toolkit + Haptics + Hover Highlight + Tracking Loss Guard
πŸ€– AI (Copilot + Cursor)

Cursor produced a well-structured GrabbableObject class in about 6 minutes. It correctly subscribed to selectEntered and selectExited events and used SendHapticImpulse on the XRBaseController. The hover highlight used a material swap β€” functional, though I'd use a shader property in production. The tracking loss guard was where it fell short: AI added a null check on the interactor but not on the controller reference itself, meaning a tracking dropout mid-grab would still throw.

// AI β€” haptics correct, null guard incomplete void OnSelectEntered(SelectEnterEventArgs args) { var ctrl = args.interactorObject as XRBaseControllerInteractor; // βœ“ checks interactor null if (ctrl == null) return; // βœ— controller?.xrController can still be null ctrl.xrController.SendHapticImpulse(0.8f, 0.1f); }
βœ“ Haptics correct values βœ“ Event subscriptions clean βœ— Tracking loss NullRef ⚠ Material swap (not shader)
πŸ§‘ Riya (18 months Unity)

Riya's implementation took 28 minutes β€” she spent time reading the XRI Toolkit docs for the haptic API, which she hadn't used before. Her null guard was complete: she checked both the interactor cast and the controller reference separately. She also unsubscribed from events in OnDisable β€” something the AI completely missed, which would cause ghost event callbacks after the object is disabled or destroyed.

// Human β€” full null chain + lifecycle clean void OnSelectEntered(SelectEnterEventArgs args) { var ctrl = args.interactorObject as XRBaseControllerInteractor; // βœ“ both levels guarded if (ctrl?.xrController == null) return; ctrl.xrController.SendHapticImpulse(0.8f, 0.1f); } void OnDisable() { // βœ“ unsubscribes β€” AI forgot this interactable.selectEntered.RemoveListener(...); }
βœ“ Full null chain guarded βœ“ OnDisable cleanup ⚠ Hover slightly verbose βœ“ No lifecycle leaks

Task 1 verdict: AI wins on speed but introduces a real XR-specific bug. Missing the OnDisable event unsubscription is something I see frequently from developers who are new to Unity's event system β€” and it means the haptic callback will fire even after the GameObject is disabled or pooled. In a shipped XR app, this causes hard-to-reproduce bugs that only surface when the object lifecycle is stressed. Riya thought about lifecycle. The AI didn't.

Task 02 β€” Debug a Broken Coroutine Scene Load

Given a broken Unity scene management script with three intentional bugs: (1) a coroutine that starts before AsyncOperation is complete β€” a timing bug that surfaces only on slower devices, (2) an AR Foundation ARSession reference that's not null-checked when tracking is temporarily lost, causing a crash on resume, and (3) a DontDestroyOnLoad manager that gets duplicated on scene reload because it's missing the singleton guard. Time to identify and fix all three.

Task 02 Β· Debug β€” Coroutine Timing + AR Tracking Null Crash + DontDestroyOnLoad Duplicate
πŸ€– AI (Copilot + Cursor)

Bug 3 (the singleton duplicate) was caught almost instantly β€” Cursor recognised the pattern immediately and suggested the standard instance guard. Bug 2 (the AR tracking null ref) was partially caught: Cursor flagged the reference but suggested wrapping it in a generic try/catch rather than properly checking ARSession.state. Bug 1 was never found. The async timing issue only manifests on devices slower than the editor β€” AI had no way to reason about the hardware timing context, and the code was syntactically valid. After 4 prompts describing the symptom ("sometimes loads before async is done"), Cursor kept suggesting adding a yield return null in the wrong place.

// AI caught Bug 3 β€” singleton guard correct void Awake() { if (instance != null && instance != this) { Destroy(gameObject); return; } instance = this; DontDestroyOnLoad(gameObject); } // Bug 1 β€” AI never found this // StartCoroutine fires before isDone check
βœ— Bug 1 missed (timing) ⚠ Bug 2 partial fix βœ“ Bug 3 found (singleton)
πŸ§‘ Riya (18 months Unity)

Riya tested in the editor first, then specifically asked me: "Is this expected to run on mobile? Because some of these timings look editor-only." That question alone was worth the whole experiment β€” she'd been burned by async timing differences between editor and device before. She correctly replaced the bare yield return asyncOp with a while (!asyncOp.isDone) loop. Bug 2 she fixed properly using ARSession.state comparison rather than a try/catch. All three found. Time: 32 minutes.

// Human β€” Bug 1 fixed correctly IEnumerator LoadScene(string name) { var asyncOp = SceneManager .LoadSceneAsync(name); asyncOp.allowSceneActivation = false; while (asyncOp.progress < 0.9f) yield return null; asyncOp.allowSceneActivation = true; yield return asyncOp; // βœ“ waits for done }
βœ“ Bug 1 found + fixed βœ“ Bug 2 proper AR fix βœ“ Bug 3 found (singleton)
⚠️ The Unity-specific debugging gap β€” this is the core insight of this entire battle

The coroutine timing bug only manifests on device, not in the editor β€” because the Unity Editor runs on a machine fast enough to complete the async operation before the next frame. On an older Android phone, it crashes on every load. AI cannot reason about this class of bug because it has no concept of the hardware execution context. Riya asked the right question: "Is this for mobile?" That question unlocks the entire debugging path. AI never asks it. This is the gap that matters most in XR development, where editor behaviour and device behaviour routinely diverge in exactly these timing-sensitive scenarios.

Task 03 β€” Unity EditorTests for a Spatial Math Utility

Given a 90-line SpatialMath static class with 5 methods: a world-to-local transform converter, a smoothed quaternion lerp (using Slerp with a configurable speed), a bounds-overlap checker for XR colliders, an angle-between-vectors utility, and a function that snaps a Vector3 to the nearest point on a plane. Write Unity EditorTests covering all 5 functions with meaningful edge cases. Target: zero test failures on first run.

Task 03 Β· Unity EditorTests β€” 5 spatial math functions, NUnit, edge cases, first-run pass
πŸ€– AI (Copilot + Cursor)

This was AI's best task. Cursor produced the full test suite in 7 minutes, structured with NUnit [TestFixture] and clear [Test] attributes. It correctly used Assert.That(..., Is.EqualTo(...).Within(0.001f)) for float comparisons β€” a Unity-specific gotcha that many junior devs miss. Edge cases were solid: identity quaternions, zero-length vectors, degenerate plane normals. One miss: the bounds-overlap test used new Bounds() without a size, creating a zero-size bounds that always returns false β€” making the test trivially pass rather than actually testing the function.

// AI β€” good float comparison practice [Test] public void AngleBetween_Perpendicular_Is90() { var result = SpatialMath.AngleBetween( Vector3.forward, Vector3.right); Assert.That(result, Is.EqualTo(90f).Within(0.001f)); } // βœ— bounds test uses zero-size β€” trivial pass
βœ“ Float tolerance correct βœ“ Edge cases solid βœ— Bounds test trivially passes βœ“ 7 min total
πŸ§‘ Riya (18 months Unity)

Riya took 26 minutes and wrote more descriptive test names β€” the kind that tell you exactly what failed when CI goes red at 2am. Her bounds test was correct: she constructed a proper Bounds with explicit center and size. She also tested the Slerp function at t=0 and t=1 boundary conditions β€” the AI only tested midpoint interpolation. One miss: she forgot to test the plane-snap function with a degenerate (zero-length) normal, which the AI had caught.

// Human β€” correct bounds test [Test] public void BoundsOverlap_WhenIntersecting_ReturnsTrue() { var a = new Bounds(Vector3.zero, Vector3.one); var b = new Bounds(Vector3.one * 0.4f, Vector3.one); // βœ“ meaningful overlap β€” not trivial Assert.IsTrue(SpatialMath.BoundsOverlap(a, b)); }
βœ“ Bounds test correct βœ“ Better failure messages ⚠ Missed degenerate normal βœ“ Slerp boundaries tested
πŸ’‘ The trivial-pass problem in AI-generated tests

The AI's zero-size Bounds test is a subtle but important failure: it wrote a test that passes without actually testing anything meaningful. This class of mistake β€” tests that pass regardless of the implementation β€” is harder to catch than a failing test because everything appears green. It's a Unity-specific variant of a general AI testing pattern: AI optimises for tests that don't fail, not for tests that are genuinely meaningful. Always read AI-generated test assertions, not just the test names.

Aggregate Numbers

35 min
AI total (3 tasks)
86 min
Human total (3 tasks)
4
AI issues found
2
Human issues found

πŸ“Š The Scorecard

Battle 03 Β· Unity/XR Coding Scorecard
3 Unity tasks Β· GitHub Copilot + Cursor vs 18-month dev Β· Scored 1–10
πŸ€– AI
πŸ§‘ Human
Winner
Speed
35 min AI vs 86 min human across 3 tasks
10
4
AI
XR Lifecycle Awareness
OnDisable cleanup, event unsub, tracking states
4
9
Human
Debugging (Device context)
Editor vs device timing awareness, AR states
3
10
Human
Test Quality
Meaningful assertions, edge coverage, no trivial passes
7
8
Human
Code Structure
Unity patterns, readability, maintainability
6
8
Human
Total
Out of 50
30/50
39/50
Human

πŸ† The Honest Verdict

πŸ† Verdict β€” Battle 03 Β· Unity/XR Coding
Human wins clearly β€” XR's complexity exposes AI's biggest blind spot

This is the largest margin of any battle in the series so far: 39 vs 30. The gap is wider than logo design or caption writing because Unity XR development has a layer of complexity that AI tools currently handle very poorly β€” the gap between editor behaviour and device behaviour.

In 13 years of XR development, I've seen this specific class of bug cause more production issues than almost anything else. Coroutines that work perfectly in the editor but crash on a mid-tier Android. AR tracking states that behave differently depending on whether the session was paused or freshly initialised. Haptic callbacks that fire on destroyed objects because nobody called RemoveListener in OnDisable. None of these are syntax errors β€” they're hardware-context errors, and AI has no hardware context.

Riya's instinct to ask "Is this for mobile?" before even looking at the coroutine bug was worth more than any amount of syntactic pattern-matching. That question comes from being burned by the same issue on a real device. AI doesn't get burned. It doesn't have a shipped product that crashed at a client demo. That experiential knowledge is not in any training dataset.

Speed remains AI's only clear win. 35 minutes vs 86 minutes across 3 tasks is significant, and for scaffolding, boilerplate, and initial structure, AI is genuinely faster. But in XR specifically, fast scaffolding with broken lifecycle management can make things slower overall β€” because you're debugging on device, which is the slowest possible feedback loop.

πŸ”€ How I Actually Use AI in My Unity Workflow

⚑ 13 years XR dev β€” here's the specific workflow that works

AI for structure, human judgment for lifecycle and device

I use Copilot every day in my Unity work. Here's exactly where I let it lead and where I take over:

01
AI scaffolds the MonoBehaviour skeleton (save ~70% of setup time): Ask Cursor to generate the initial class with the right events, fields, and method stubs. It handles XRI Toolkit event wiring well. Accept the scaffold, then read every line before running it β€” don't just compile and trust.
02
Manually add OnDisable / OnDestroy cleanup before anything else: Make this a physical reflex. Every time AI gives you an event subscription, immediately write the corresponding unsubscribe in OnDisable. AI never does this reliably. It's 2 lines and it prevents a category of ghost-callback bugs that are nightmarish to trace.
03
Describe bugs to AI with device context explicitly stated: Don't just say "the scene doesn't load." Say "this is targeting Android, the issue only happens on device not in editor, the async load starts before the previous scene is fully unloaded." That context transforms AI's debugging response from generic to useful.
04
Use AI for EditorTests but read every Assert: Let AI generate the suite fast, then manually review each assertion. Look for trivial passes: zero-size Bounds, identity transforms that always pass, unchecked float equality without tolerance. Reading takes 5 minutes. Missing a trivial pass wastes hours of false confidence.
05
Never let AI handle AR Foundation state logic unsupervised: ARSession state, tracking state transitions, and camera pose confidence are where AR apps most commonly crash in production. AI doesn't understand the state machine intuitively. Write that part yourself, then ask AI to help you write tests for it.

With this workflow, AI saves me approximately 40–50% of feature development time β€” primarily on scaffolding, boilerplate, and test writing. The parts I still write manually are the parts that have burned me on real shipped projects: lifecycle management, AR state handling, and anything that behaves differently on device vs editor.

When to use each approach in Unity XR

πŸ€– Use AI when…
  • Scaffolding a new MonoBehaviour or ScriptableObject
  • Setting up XRI event wiring and initial field declarations
  • Generating EditorTests for pure logic / math utilities
  • Converting C# patterns between Unity versions
  • Writing shader property wrappers and material helpers
πŸ§‘ Apply human judgment for…
  • OnDisable / OnDestroy / event unsubscription β€” always
  • Any coroutine involving async scene operations on device
  • AR Foundation tracking state and session lifecycle
  • Haptic, audio, and visual feedback β€” needs tuning on device
  • Any bug that only manifests on hardware, not in editor

Series tally after 3 battles

Human wins all three: Logo Design (39 vs 32), Instagram Captions (37.4 vs 35.8), Unity Coding (39 vs 30). The coding battle produced the widest margin β€” not because AI is bad at Unity, but because Unity XR development has a class of hardware-context bugs that sit completely outside what LLMs can reason about. The pattern across all three battles remains consistent: AI wins when the task is well-defined and lives entirely in a single context. Human wins when the task requires reasoning about a context the AI has never experienced β€” a real brand, a specific audience, a device that crashes differently than the editor.