Antigravity × Unreal Engine 5 Plugin Production Guide — Designing Reusable Game Systems with AI for Multiple Titles
A production-grade guide to building UE5 plugins with Antigravity. Covers module design, build configuration, AI-friendly code separation, and distribution patterns for reusing one plugin across multiple titles.
After your second or third UE5 project, a pattern starts to repeat. Inventory. Save system. Settings menu. Achievement unlocks. UI frame. The contents differ slightly from game to game, but the structure is almost always the same. You know you should pull these out into a plugin, but every attempt seems to get stuck on .Build.cs dependency graphs or that endless Public/Private split — and you end up pasting the code into the game module again. I've been there more times than I'd like to admit.
Antigravity has changed that loop for me. The agent will happily suggest Build.cs lines and header visibility, which lowers the psychological cost of starting a plugin. But — and this is the catch — accepting AI suggestions blindly will break your build in subtle ways. You need to design your plugin so that the agent has the right context to give good suggestions. That's what this guide is about.
We'll build three reusable systems — inventory, save, and achievements — as a single plugin called CommonGameSystems, then walk through how to ship and version it across multiple titles. The code is complete and drop-in ready, not a sketch.
Why "AI-friendly plugin structure" matters
Antigravity reads AGENTS.md and your file layout to decide what to generate. If you point it at a UE5 project, it tends to treat Source/<GameName>/ as one big module. Plugin headers under Plugins/ get read as separate context, and the agent often loses track of which symbols are exported and which are not.
The biggest casualty is the Public//Private/ split. UE5 only allows other modules to #include headers under Public/. Ask Antigravity to "write code that adds an item to the inventory" without giving it that boundary, and it will happily reach into a Private/ struct from your game module — which compiles in the plugin but fails to link in the consuming module with unresolved external symbol.
The fix is simple: divide the plugin clearly into an "API zone" and an "implementation zone," then write that division down in an AGENTS.md the agent can read. After this one change, my agent suggestions stopped being broken almost overnight.
Plugin skeleton: .uplugin and .Build.cs
Start with the file layout. We'll put the plugin at Plugins/CommonGameSystems/:
The split between Runtime and Editor is non-negotiable for shipping. Editor modules are stripped from packaged builds, so they're the right home for editor-only tools and asset actions. Skip this split early on and you'll spend a frustrating afternoon hunting down "missing Slate" or "unresolved UnrealEd" errors at packaging time.
The LoadingPhase of Default is intentional. Earlier phases like PreDefault run before subsystems such as UAssetManager are ready, and any AI-generated code that touches them will crash on game start. I've seen this happen on multiple projects — Antigravity sometimes proposes PreDefault because it sounds more "earnest," but it's almost always wrong for gameplay code.
The .Build.cs is the file Antigravity gets wrong most often. Keep this template at the top of the file so you can spot incorrect dependency additions in your diff:
// Copyright (c) Your Studio. All Rights Reserved.using UnrealBuildTool;public class CommonGameSystemsRuntime : ModuleRules{ public CommonGameSystemsRuntime(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; bUseUnity = false; // Faster per-file builds during plugin dev — turn on later PublicIncludePaths.AddRange(new[] { "CommonGameSystemsRuntime/Public" }); PrivateIncludePaths.AddRange(new[] { "CommonGameSystemsRuntime/Private" }); PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "GameplayTags", // identify item types via tags "DeveloperSettings", // data-driven settings }); PrivateDependencyModuleNames.AddRange(new[] { "Json", "JsonUtilities", // save data serialization "OnlineSubsystem", // achievements }); }}
bUseUnity = false is a stylistic choice for production, but during plugin development it's a quality-of-life win. Single-file builds make it much easier to localize the file that broke when the AI's last suggestion didn't compile. Flip it back to true once the plugin stabilizes.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Structure your UE5 plugin's Public/Private boundary so Antigravity stops generating broken Build.cs files and unresolved external symbols
✦Walk away with three working systems — inventory, save, and achievements — complete with .uplugin and .Build.cs ready to drop into your project
✦Design version management, build isolation, and module dependency rules that hold up when you reuse a single plugin across two, three, or more shipped titles
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
System 1: Inventory — designing the API as a UActorComponent
Inventory is the system that tends to look the same across genres, so it's the natural starting point. Here's the public header (Public/Inventory/InventoryComponent.h):
Never drop the COMMONGAMESYSTEMSRUNTIME_API macro. Without it, the consuming module can call AddItem from header but fail to link with unresolved external symbol. Antigravity forgets this constantly — I run a habitual grep -n "_API " *.h over generated headers before committing.
Calling AddItem(Tag.Item.Health, 5) should fire OnInventoryChanged once and leave GetQuantity returning 5. When stack limits are hit, you'll see one broadcast per new slot opened.
Why GameplayTag over an enum? Tags branch cleanly in Blueprint, pair well with Data Tables, and — crucially — let your designers add new item types without forcing a C++ recompile. Enums force every new item to drag the entire team back through a build.
System 2: Save — implementing as a UGameInstanceSubsystem
USaveGame is fine on its own, but managing slot names, async writes, and load timing in every game gets tedious. A subsystem wrapper means your gameplay code becomes a one-liner: SaveSubsystem->SaveSlotAsync("AutoSave", OnDone).
A note on error handling: AsyncSaveGameToSlot does not throw on disk-full or permission errors. It quietly returns bOk = false. If your game ignores that flag, you'll ship a "save succeeded silently but didn't" bug — the worst kind. Make it a team rule that the OnDone callback always pipes into a UI notification on failure. Antigravity tends to drop this callback handling unless you're explicit; it's a great line to write into your AGENTS.md.
System 3: Achievements — a thin wrapper over OnlineSubsystem
Achievement APIs differ across Steam, Epic, PSN, and Xbox just enough to be annoying. Wrap them in a thin UAchievementSubsystem and hide the store-specific code in Private/. Your gameplay code becomes AchievementSubsystem->Unlock("FirstClear") and stays portable.
The implementation grabs IOnlineAchievementsPtr and queues anything that fails locally, retrying via FlushPending once the subsystem comes online. There's a real trap here: OnlineSubsystem's Get() returns nullptr regularly — offline launches, store auth errors, dev builds without credentials. Code that does if (!OSS) return; and forgets the local queue ships a bug where achievements never unlock. Antigravity will write that exact pattern unless you tell it not to. This is the single biggest review-time pitfall in this whole plugin.
Telling Antigravity about your plugin: an AGENTS.md
Place an AGENTS.md directly inside the plugin folder — not the project root. Antigravity will pick it up as plugin-local context and apply the rules only when generating code in that folder.
# CommonGameSystems plugin — agent instructions## Module boundaries- Public API: `Plugins/CommonGameSystems/Source/CommonGameSystemsRuntime/Public/`- Internal: `Plugins/CommonGameSystems/Source/CommonGameSystemsRuntime/Private/`- Editor-only: `Plugins/CommonGameSystems/Source/CommonGameSystemsEditor/`## Rules for code generation1. Every public type/function MUST have `COMMONGAMESYSTEMSRUNTIME_API`2. Public/ headers MUST NOT `#include` anything from Private/3. Adding a new dependency to Build.cs requires a comment explaining why4. Any OnlineSubsystem code MUST null-check Get() and queue locally on failure## Naming conventions- Subsystems: `UXxxSubsystem` (derive from GameInstanceSubsystem)- Actor components: `UXxxComponent`- Blueprint-callable functions: `BlueprintCallable` or `BlueprintPure`## Forbidden- Including `<GameName>/...` headers from inside this plugin (creates circular deps)- Referencing Editor-module headers from Runtime modules
In practice, this single file cuts down the "missing API macro" and "forgot to add Slate to the Editor module" mistakes by a wide margin. The agent's suggestions become noticeably more buildable.
Common pitfalls
These are the ones that cost me a real afternoon at some point.
Mixing up PublicDependencyModuleNames and PrivateDependencyModuleNames. If a header in Public/#includes a type, that type's module must be in PublicDependencyModuleNames. Antigravity often files things under Private because "the implementation uses it," which is wrong — the consuming module needs visibility too. The symptom is "missing header" errors when the game module tries to use your plugin.
LoadingPhase: PreDefault. Subsystem code that touches UAssetManager or UGameInstance will crash because those aren't initialized yet. Default unless you have a written reason otherwise.
Forgetting bUseUnity = false on the Editor module. Unity Build smears transitive Editor-only dependencies (like UnrealEd) into headers that then leak into runtime code, which packages cleanly in editor but fails on Win64 packaged builds with "missing Slate" errors. Set both modules to bUseUnity = false initially, then flip them when stable.
Distributing the plugin: versioning and reuse
Once you're shipping the plugin to two or more games, version management becomes the actual bottleneck. My recommendation: keep the plugin in its own Git repository, and pull it into each game project either as a git submodule or a CI checkout under Plugins/.
Use semantic versioning in VersionName, and reference the version explicitly from each game's .uproject:
UE5 doesn't strictly validate MarketplaceURL, but Antigravity reads it. Once you've written the version into .uproject, the agent has the context "this game pins CommonGameSystems 1.0.0" and stops suggesting code that depends on features you haven't shipped yet.
For ABI compatibility: when you add a member to a USTRUCT exposed in Public/, append it at the end and mark Blueprint-visible flags as BlueprintReadOnly. Inserting members in the middle, or reordering enum values, will silently corrupt save data from older builds. Drop this rule into AGENTS.md to stop the agent from "tidying up" your structs.
CI builds and Antigravity automation
If you're going to reuse the plugin across titles, CI is no longer optional — you need to build the plugin standalone whenever someone changes it. Here's a starting point with GitHub Actions and RunUAT.bat:
Pair this with Antigravity's Browser Sub-Agent watching https://github.com/yourstudio/CommonGameSystems/actions — when a build fails, the agent can summarize the error log and post to Slack while you stay in the game project. I run this exact loop on my own studio's plugin and it's caught more than one weekend regression.
Unit-testing the plugin with the UE5 Automation Framework
Plugins that ship to multiple titles need tests, and UE5's Automation Framework is a reasonable home for them. Add a tiny test module to the plugin and Antigravity can generate test cases from your AGENTS.md rules.
// Source/CommonGameSystemsTests/CommonGameSystemsTests.Build.csusing UnrealBuildTool;public class CommonGameSystemsTests : ModuleRules{ public CommonGameSystemsTests(ReadOnlyTargetRules Target) : base(Target) { bUseUnity = false; PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "CommonGameSystemsRuntime", // <- the module under test }); PrivateDependencyModuleNames.AddRange(new[] { "AutomationController" }); }}
A simple test that verifies inventory stacking:
// Private/InventoryStackingTest.cpp#include "Misc/AutomationTest.h"#include "Inventory/InventoryComponent.h"IMPLEMENT_SIMPLE_AUTOMATION_TEST( FInventoryStackingTest, "CommonGameSystems.Inventory.Stacking", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::SmokeFilter)bool FInventoryStackingTest::RunTest(const FString& Parameters){ UInventoryComponent* Inv = NewObject<UInventoryComponent>(); FGameplayTag Tag = FGameplayTag::RequestGameplayTag(FName("Item.Health")); // Add 250 health potions; should split across 3 slots (99, 99, 52) TestTrue(TEXT("Add 250"), Inv->AddItem(Tag, 250)); TestEqual(TEXT("Total"), Inv->GetQuantity(Tag), 250); // Remove 100; remaining should be 150 TestTrue(TEXT("Remove 100"), Inv->RemoveItem(Tag, 100)); TestEqual(TEXT("After remove"), Inv->GetQuantity(Tag), 150); return true;}
Open the Session Frontend in editor (Window → Test Automation), run the CommonGameSystems filter, and the test executes in-process. Antigravity is good at proposing additional cases like "remove from empty inventory" or "add zero quantity" once it sees this pattern. Ask it to "expand the inventory tests for failure cases" and review the diff — you'll typically get four to six new edge cases that are worth keeping.
The one gotcha: tests in this module link against CommonGameSystemsRuntime, which means a broken API change in the runtime module immediately breaks the test build. That's the point — failing tests catch ABI-level mistakes before they ship.
Iteration speed: hot reload and live coding
UE5's Live Coding lets you patch C++ in a running editor session without restarting. For plugin development this is huge — a typical inventory tweak goes from a 90-second compile-link-restart cycle to a 5-second Ctrl+Alt+F11 patch.
Two configuration points matter for plugin work:
In Editor Preferences → General → Live Coding, enable "Enable Live Coding" and set "Restart" to "When required" (not "Always"). With the latter, header changes still force restarts; the former lets the engine patch in place when it can.
In the plugin's Build.cs, keep bUseUnity = false while you're iterating. Live Coding patches per .cpp file, so unity builds give you fewer "did anything actually compile?" moments.
There's a real pitfall here: Live Coding doesn't pick up changes to UPROPERTY flags or USTRUCT member layout. If you change a struct's serialization, you must close and reopen the editor — otherwise saved data and live data drift apart and you get bizarre crashes. Antigravity occasionally suggests UPROPERTY reorderings as "cleanups." Reject those when iterating; do them at module boundaries when you're ready to bump the plugin's VersionName.
Performance: when the inventory hits 10,000 items
The implementation above is fine for typical RPG inventories (50–500 slots). If you're building a survival or factory game where inventories run into the thousands, the linear-scan AddItem becomes a bottleneck. The realistic mitigation is to keep an auxiliary TMap<FGameplayTag, TArray<int32>> mapping each tag to the slot indices that hold it.
Antigravity can write this refactor surprisingly well if you give it the constraint as a comment at the top of the file:
// PERF: This component is used in survival games with up to 10,000 slots.// Keep AddItem/RemoveItem/GetQuantity at O(1) average for a given tag by// maintaining an auxiliary index. Items[] remains the source of truth.
When I added that comment and asked the agent for a refactor, it produced a clean implementation that maintained both data structures atomically and updated the index in RemoveAt. The one thing it forgot was rebuilding the index after LoadSlot — review-time catch.
Marketplace publishing: what actually changes
If you're considering listing the plugin on Fab (the successor to UE Marketplace), three additional constraints kick in:
The plugin must build cleanly on the exact engine versions you list. Multi-version support means CI matrices, and at that point Antigravity earns its keep generating per-version conditional code (#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4).
All third-party dependencies must be license-cleared. The Json and OnlineSubsystem dependencies in our example are first-party, so we're fine; if you add nlohmann/json, that's a different conversation.
Documentation is mandatory and reviewers actually read it. Use Antigravity to generate a baseline README.md from your AGENTS.md and then edit it for tone — pure AI-generated docs read as such and reviewers will push back.
This isn't a path I personally take for every plugin I write — for a studio's internal-only plugin, the overhead isn't worth it. But knowing the constraints up front saves a lot of refactoring if you decide to publish later.
Cross-platform development: Linux, Mac, and Windows in one repo
UE5 plugins regularly need to compile on Windows, Mac, and Linux when you're shipping to consoles or working with a remote team. The plugin we've built compiles cleanly on all three out of the box, but a few patterns are worth establishing now to avoid pain later.
The biggest cross-platform pitfall in plugin code is path-string handling. UE5's FPaths::ConvertRelativePathToFull and FPaths::Combine handle separators correctly across OSes; raw FString concatenation with "\\" does not. Antigravity will sometimes write Windows-flavored paths because of training-data bias, especially when generating editor-side asset utilities. The fix is a one-line AGENTS.md rule: "All filesystem paths must use FPaths::Combine — never raw separators."
For continuous integration, mirror your plugin-build.yml for each platform you support:
The matrix expands to four jobs and gives you confidence that all four combinations build before you tag a release. In my experience, about one in five plugin updates breaks one of these combinations in a non-obvious way — usually because of a header that compiles on Windows but trips Clang's stricter rules on Mac.
For Linux, the official unreal-engine-linux container images aren't yet available on GitHub Actions hosted runners, so you'll typically run them on a self-hosted runner with the Linux engine SDK installed. This is the one place where the operational cost is real, but for most studios the Win64 + Mac matrix is sufficient at the start.
Operational hygiene: log channels and crash diagnostics
The last thing worth building into a production plugin from day one is a dedicated log channel. Adding DECLARE_LOG_CATEGORY_EXTERN(LogCommonGameSystems, Log, All); to your runtime module and routing all UE_LOG calls through it pays off the moment a customer reports a bug — you can pull just your plugin's log output instead of grepping through LogTemp noise.
Place this in CommonGameSystemsRuntime.h (the module's "main" public header):
Then update every UE_LOG(LogTemp, ...) in the plugin to use LogCommonGameSystems. Antigravity is good at this kind of mechanical refactor — feed it the plugin folder and ask "replace all LogTemp references in this module with LogCommonGameSystems," and it will do exactly that, including catching cases inside lambdas and macros.
For crash diagnostics, integrate with UE5's FGenericCrashContext if you ship to PC. The plugin can register a callback that adds plugin-level metadata (current version, last save slot, last achievement progress) to the crash dump — invaluable when triaging customer reports.
When NOT to extract a system into a plugin
Plugins are the right answer most of the time once you're shipping multiple titles, but a few patterns suggest the inline path is still better.
If a system is deeply coupled to the specific game's economy or progression curves — for example, a quest system whose state machine references game-specific narrative beats — extracting it forces you to invent generic abstractions that don't carry their weight. The "plugin overhead" tax (Build.cs, Public/Private split, version management) costs you more than the duplication does. Antigravity is good at suggesting these extractions early, but resist if the abstraction feels strained.
Another signal to keep code inline: the system is a one-off you'll only use in this game. Map travel logic, a credits scroller, the title-screen attract loop. There's no reuse story, so all of the plugin scaffolding is pure overhead. The bar I use is "would I copy this folder into a brand-new game project tomorrow without changes?" If the answer is "yes, with one or two parameters tweaked," it belongs in a plugin. If it's "yes, but I'd rewrite half of it for the new game's mechanics," it doesn't.
Finally: if the team is one or two people and you have a clear single shipping target, don't preemptively pluginize. The cost shows up as "AI suggests changes that span the plugin boundary, and you have to keep flipping between two folders" — friction that costs real time on small teams. Plugin extraction has the best ROI when (a) you've shipped at least once, (b) you have a second project committed, and (c) you can dedicate a half-day to the migration. Otherwise, ship the game first.
Closing thoughts
Pulling things out into a plugin always feels heavier than it should the first time. If you're only ever shipping one game, the inline path is faster. The moment you start a second game, the math flips — every reusable system you have ready saves you hours.
Antigravity is genuinely good at the parts of this work that humans are bad at: keeping Build.cs consistent, remembering API macros, writing AGENTS.md rules that get respected. My advice is to start small. Carve out one system from the game you're working on right now — inventory is the easiest — and put it in a folder under Plugins/. One header, one Build.cs, an hour or two of work to get it building. Then let the agent help you refine.
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.