ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-03-10Intermediate

Unity × Antigravity — Which Game Dev Tasks to Hand Off to AI, and Which to Keep

A hands-on look at how far Antigravity can take you in Unity, with real code. Player controllers, enemy state machines, shader drafts, and Update-loop allocation hunting — where the AI shines and where it doesn't.


As an indie developer who also runs the Dolice Labs sites, I've been rebuilding a small Unity game in my spare time alongside the apps I ship solo. The first thing I noticed after bringing Antigravity into that workflow was that it neither "does everything" nor "is useless" — its strengths and weaknesses split cleanly by task.

Rather than abstractly claiming "AI is handy in Unity," I'll show the exact prompts I sent, the code that came back, and the parts I fixed by hand. Where the AI is worth handing work to, and where it's faster to write it yourself, is the whole point of this article.

A player controller comes back "80% done"

I started with the classic: player movement. My prompt was, "A CharacterController script: move with WASD, jump with Space, variable jump height based on how long the button is held, plus ground detection." Here's what came back.

using UnityEngine;
 
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    [SerializeField] float moveSpeed = 6f;
    [SerializeField] float jumpForce = 7f;
    [SerializeField] float gravity = -20f;
    [SerializeField] float jumpCutMultiplier = 0.5f; // damp rise on early release
 
    CharacterController cc;
    Vector3 velocity;
 
    void Awake() => cc = GetComponent<CharacterController>();
 
    void Update()
    {
        bool grounded = cc.isGrounded;
        if (grounded && velocity.y < 0) velocity.y = -2f; // press into the floor when grounded
 
        float x = Input.GetAxisRaw("Horizontal");
        float z = Input.GetAxisRaw("Vertical");
        Vector3 move = transform.right * x + transform.forward * z;
        cc.Move(move.normalized * moveSpeed * Time.deltaTime);
 
        if (grounded && Input.GetButtonDown("Jump"))
            velocity.y = jumpForce;
 
        // release the jump button early to cut the rise = variable jump
        if (Input.GetButtonUp("Jump") && velocity.y > 0)
            velocity.y *= jumpCutMultiplier;
 
        velocity.y += gravity * Time.deltaTime;
        cc.Move(velocity * Time.deltaTime);
    }
}

The skeleton is fine. Implementing variable jump by damping upward velocity the moment the button is released is a clean, readable approach.

The catch: as written, diagonal movement is normalized with move.normalized, but it adds transform.right and transform.forward, so it drifts once the camera tilts. My case needed camera-relative movement, so I added a step to project the input vector onto the camera's facing direction before normalizing. The AI gets you to "moves with a CharacterController," but it can't read your game's specific camera design. That gap is what "80% done" really means.

Hand the enemy AI the state-machine skeleton

For enemy behavior, generating the skeleton and filling it in beat writing from scratch. Asking for "patrol → detect player → chase → on lost sight go to last position → return to patrol after a timeout, as an enum state machine" yields a template that rarely misses a branch.

public enum EnemyState { Patrol, Chase, Investigate }
 
public class EnemyBrain : MonoBehaviour
{
    [SerializeField] float sightRange = 12f;
    [SerializeField] float loseSightAfter = 3f;
    EnemyState state = EnemyState.Patrol;
    float lostTimer;
    Vector3 lastSeen;
 
    void Update()
    {
        bool canSee = CanSeePlayer();
 
        switch (state)
        {
            case EnemyState.Patrol:
                Patrol();
                if (canSee) state = EnemyState.Chase;
                break;
 
            case EnemyState.Chase:
                ChasePlayer();
                if (canSee) { lastSeen = PlayerPos(); lostTimer = 0; }
                else { lostTimer += Time.deltaTime;
                       if (lostTimer > loseSightAfter) state = EnemyState.Investigate; }
                break;
 
            case EnemyState.Investigate:
                MoveTo(lastSeen);
                if (canSee) state = EnemyState.Chase;
                else if (ReachedLastSeen()) state = EnemyState.Patrol;
                break;
        }
    }
}

What helps here is that the AI lays out the easy-to-forget "what happens after losing sight" as a slot up front. The bodies of CanSeePlayer() and Patrol() depend on project specifics — raycast layer masks, whether you use a NavMesh — so you end up writing those yourself. Skeleton from the AI, internals by hand: that division meshed best.

With shaders, expect a draft and no more

For common effects like a damage flash, having Antigravity sketch a draft saves time. I asked for "not URP Shader Graph, but HLSL — a simple fragment that pushes color toward white/red on hit." Here's the result.

// simple hit flash (_FlashAmount from 0 to 1)
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    fixed3 flash = lerp(col.rgb, fixed3(1, 0.2, 0.2), _FlashAmount);
    return fixed4(flash, col.a);
}

The idea is sound. But shader syntax and variable names change with Unity's render pipeline (Built-in / URP / HDRP). The AI sometimes mixes pipelines — even when I said URP, it slipped in Built-in-style code. I use it on the assumption that I'll take the draft and adapt it to my pipeline. Trusting a whole shader as-is is still premature, honestly.

Performance investigation is where the AI earns its keep

The surprise was that pinpointing bottlenecks turned out more reliable than code generation. Even a vague "Update is heavy, the frame rate drops" gets Antigravity to list the usual suspects in order.

In my code, for instance, it flagged a spot calling GetComponent every frame inside Update, and another that rebuilt a list with new each frame. Cache the first, reuse the second, and the allocations vanish.

// Before: per-frame GetComponent + new list = generates GC
void Update() {
    var rb = GetComponent<Rigidbody>();
    var hits = new List<Collider>(Physics.OverlapSphere(transform.position, 3f));
    // ...
}
 
// After: cache the reference, reuse the buffer
Rigidbody rb;
readonly Collider[] buffer = new Collider[16];
void Awake() => rb = GetComponent<Rigidbody>();
void Update() {
    int n = Physics.OverlapSphereNonAlloc(transform.position, 3f, buffer);
    // ...
}

Steering toward allocation-free APIs like OverlapSphereNonAlloc is the kind of move you won't reach for unless you're used to reading the docs. When the profiler lights up red and I can't see why, I now describe the situation to Antigravity first to narrow it down, then confirm with the Unity Profiler — and that order made investigation faster.

Where handing off to the AI didn't pay off

To be honest, some tasks clearly don't benefit.

One is game feel. How good the jump feels, the weight of a hit, the softness of camera follow — those numbers you can only settle by playing it and feeling it with your own hands. Asking the AI to "make it feel good" doesn't land; that's yours to decide by playing.

The other is scene structure and prefab design. Which objects to parent, where to carve out a prefab — that depends heavily on how you plan to extend the game. Leaving it to the AI tended to produce a structure that's hard to revise later. I've settled on keeping the design decisions myself and handing the AI the implementation legwork.

Your next step

If you're adding Antigravity to Unity, I'd suggest not asking for a big feature right away — try it once on a small problem with a known-good answer, like a player controller. Jot down what you had to fix in the returned code, and you'll start to see the context the AI can't read in your project: camera design, render pipeline, naming conventions. Once you know that boundary, deciding what to hand off and what to write by hand gets far faster.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-07-18
Which Device Gets the iOS 27 Public Beta? Choosing a Test Phone When There's No Way Back
The iOS 27 public beta arrived on July 13 carrying the same build number as developer beta 3. Starting from that one detail, here is how I decide which device gets it, what to archive first, and where to look in my own app.
App Dev2026-07-18
After Compose-First: Choosing Which View Screens to Migrate, Ranked by Churn Instead of Count
Google has declared Android development Compose-first. Here is how I rank View-based screens for migration using git history rather than screen counts, with the scoring script I actually ran and the three places partial migration quietly duplicates state.
App Dev2026-07-17
Android 17's Local Network Permission: Inventory Every LAN Call Site Before You Hand the Fix to an Agent
Apps targeting the new API level need explicit permission to reach the local network. A grep for private IPs found 2 of my 11 LAN paths. Here is the detector that found all 11, and how I turned its output into eleven checkable tasks for an agent.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →