Filling a game with content is slow. Fifty items, twenty enemies and a few hundred lines of dialogue can take longer than writing the systems that use them. AI is a fast way to get a first draft, as long as you treat it as a draft. This guide shows how to use AI to generate game content for Unity: get items, enemies and dialogue as clean JSON, import them into ScriptableObjects with an editor script, validate the data, and sanity-check the balance before it reaches your players.
Why JSON in the Middle
You could ask an AI to write a .asset file directly, but Unity’s YAML asset format includes GUIDs and script references that the AI has to guess. JSON is a better middle step:
- AI models produce JSON reliably when you give them a schema.
- You can review and diff it in git like any text file.
- One importer script turns it into real ScriptableObject assets that the rest of your game already uses.
- You can regenerate or hand-edit the JSON and re-import without breaking references.
If ScriptableObjects are new to you, our ScriptableObjects guide explains why they suit item and enemy data so well.
Define the ScriptableObject First
Decide your data shape before you prompt. The ScriptableObject is the source of truth, and the JSON should mirror it.
// Assets/Scripts/Content/ItemDefinition.cs
using UnityEngine;
public enum Rarity { Common, Uncommon, Rare, Epic }
[CreateAssetMenu(menuName = "Game/Item Definition")]
public class ItemDefinition : ScriptableObject
{
public string id;
public string displayName;
[TextArea] public string description;
public Rarity rarity;
public int value;
public int damage;
public float weight;
public Sprite icon; // assigned by hand, never from JSON
}
Two decisions here save pain later:
- A stable string id like
iron_sword. The importer uses it as the file name, so re-imports update the same asset instead of creating duplicates. - Art stays manual. Icons, prefabs and audio are references the AI can’t know. Keep them out of the JSON and assign them in the Inspector. Re-imports won’t touch them.
Prompting for Valid JSON
Give the AI the exact shape, the allowed values, and your design constraints. Here is a prompt that works across ChatGPT, Claude, Copilot and similar tools:
Generate 20 weapons for a low-fantasy action RPG as JSON.
Output ONLY raw JSON, no markdown fences, no comments, no trailing
commas. Use exactly this shape:
{
"items": [
{
"id": "snake_case_unique",
"displayName": "string, max 24 chars",
"description": "string, one sentence, max 120 chars",
"rarity": "Common | Uncommon | Rare | Epic",
"value": integer,
"damage": integer,
"weight": number
}
]
}
Design rules:
- Rarity spread: 8 Common, 6 Uncommon, 4 Rare, 2 Epic.
- damage ranges: Common 4-8, Uncommon 8-13, Rare 13-19, Epic 19-26.
- value is roughly damage x 10, times 1.5 for Rare and 2.5 for Epic.
- weight 0.5 to 6.0. Heavier weapons hit harder within their rarity.
- No real-world brand names. Tone: grounded, slightly grim.
The top-level object with an items array is deliberate. Unity’s JsonUtility can’t parse a bare array at the top level, so the wrapper saves you a confusing error. Rarity as a string is also deliberate: JsonUtility stores enums as numbers, and “Rare” is far easier to review than 2. The importer converts it.
For more than about 30 entries, generate in batches and give the AI the ids it already used. Long outputs are where duplicates and drifting rules creep in.
The Editor Importer Script
Save the JSON as Assets/Content/Data/items.json. This editor script reads it and creates or updates one asset per item. It must go in an Editor folder (or an Editor-only assembly), because it uses UnityEditor.
// Assets/Scripts/Editor/ItemJsonImporter.cs
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public static class ItemJsonImporter
{
const string JsonPath = "Assets/Content/Data/items.json";
const string OutputFolder = "Assets/Content/Items";
[Serializable]
class ItemRow
{
public string id;
public string displayName;
public string description;
public string rarity;
public int value;
public int damage;
public float weight;
}
[Serializable]
class ItemFile
{
public ItemRow[] items;
}
[MenuItem("Tools/Content/Import Items From JSON")]
static void Import()
{
if (!File.Exists(JsonPath))
{
Debug.LogError($"No file at {JsonPath}");
return;
}
var file = JsonUtility.FromJson<ItemFile>(File.ReadAllText(JsonPath));
if (file == null || file.items == null)
{
Debug.LogError("JSON has no \"items\" array.");
return;
}
if (!AssetDatabase.IsValidFolder(OutputFolder))
{
Directory.CreateDirectory(OutputFolder);
AssetDatabase.Refresh();
}
var seen = new HashSet<string>();
int created = 0, updated = 0;
foreach (var row in file.items)
{
if (string.IsNullOrWhiteSpace(row.id))
{
Debug.LogWarning("Skipped an item with no id.");
continue;
}
if (!seen.Add(row.id))
{
Debug.LogWarning($"Duplicate id '{row.id}' skipped.");
continue;
}
if (!Enum.TryParse(row.rarity, true, out Rarity rarity) ||
!Enum.IsDefined(typeof(Rarity), rarity))
{
Debug.LogWarning($"{row.id}: unknown rarity '{row.rarity}', using Common.");
rarity = Rarity.Common;
}
string path = $"{OutputFolder}/{row.id}.asset";
var item = AssetDatabase.LoadAssetAtPath<ItemDefinition>(path);
bool isNew = item == null;
if (isNew) item = ScriptableObject.CreateInstance<ItemDefinition>();
item.id = row.id;
item.displayName = row.displayName;
item.description = row.description;
item.rarity = rarity;
item.value = row.value;
item.damage = row.damage;
item.weight = row.weight;
if (isNew)
{
AssetDatabase.CreateAsset(item, path);
created++;
}
else
{
EditorUtility.SetDirty(item);
updated++;
}
}
AssetDatabase.SaveAssets();
Debug.Log($"Items imported: {created} created, {updated} updated.");
}
}
Why it’s built this way:
- Update in place. Existing assets are loaded and changed, not deleted and recreated. The asset keeps its GUID, so every prefab, loot table and shop that references it keeps working, and the hand-assigned icon survives.
- The IsDefined check.
Enum.TryParseaccepts number strings, so “7” would parse into a rarity that doesn’t exist. The second check catches that. - Removed items are left alone. If an id disappears from the JSON, the old asset stays. Delete it by hand after checking nothing uses it.
You can of course ask the AI to write this importer for your own data types. Give it this one as the pattern. Our guide to building editor tools with AI covers the Editor folder rules and Undo support in more depth.
🧰 Rather not build the pipeline yourself?
Some Asset Store tools handle data tables, spreadsheet import and inventory databases out of the box, which is worth it once your content grows past a few dozen entries. We compared the tools that save indie devs the most time.
Validating What the AI Gave You
AI follows most of your rules most of the time. “Most” isn’t good enough for game data. Common problems:
- Duplicate ids or names, especially in long lists.
- Numbers just outside the ranges you asked for.
- The wrong count per rarity.
- Descriptions over your length limit, which then overflow your UI.
- Markdown fences or a friendly sentence before the JSON.
The importer already rejects bad ids and rarities. Add range checks for the rules you care about, either in the importer or as a separate “Validate Items” menu item that loads all ItemDefinition assets and logs anything out of range. Logging with the asset as the context, Debug.LogWarning(message, item), means clicking the Console entry selects the bad asset.
Also read the content. AI-written names and descriptions can be bland, repetitive (“a sturdy blade forged by…” twenty times), or accidentally close to existing games’ items. Edit freely. It’s a first draft.
Sanity-Checking Balance
An AI can’t playtest your game, and it’s weak at arithmetic across a big table. Use it to propose numbers, then check them with simple tools:
- Use a formula as the backbone. In the prompt above, value follows damage. Ask the AI to stay close to the formula rather than invent every number, so outliers are easy to spot.
- Look at it as a table. Paste the JSON into a spreadsheet, sort by damage within each rarity, and chart damage per weight or value per damage. An Uncommon sword that beats every Rare one stands out immediately.
- Ask the AI to critique, not generate. A second prompt like “Here is my weapon table. Find items that are strictly better than another item at the same or lower rarity, and any rarity tier that overlaps the next one” works well, because it’s a checking task.
- Playtest the extremes. Try the weakest and strongest item in each tier in game. Numbers that look fine on paper often feel wrong.
The same approach works for enemies: give the AI a budget formula for health, damage and speed per level, and check the curve rather than each entry.
Dialogue and Flavour Text
Dialogue works the same way. Define a node shape, such as an id, a speaker, text and a list of choices that point to other node ids, then ask for JSON in that shape. If you need a system to play it, our dialogue system tutorial uses a ScriptableObject-based format you can target directly.
Extra checks for dialogue:
- Broken links. Choices that point to node ids that don’t exist. Validate this in the importer.
- Dead ends and loops where a conversation can’t finish.
- Voice drift. Give each character a short style note in the prompt and generate one character’s lines at a time.
- Localization. Keep text in the data, not in code, so it can be exported for translation later.
Troubleshooting
“ArgumentException: JSON must represent an object type.”
The JSON starts with [. JsonUtility needs an object at the top level. Wrap the array: { "items": [ ... ] }.
“ArgumentException: JSON parse error: Invalid value.”
Usually markdown fences (```json), a sentence before the JSON, comments, or a trailing comma. Strip them, or ask the AI to output raw JSON only. Paste the file into any JSON validator to find the exact line.
All fields import as 0 or empty
The JSON keys don’t match the C# field names. JsonUtility is case-sensitive and matches names exactly, so display_name won’t fill displayName. Also check the row class is marked [Serializable] and its fields are public.
References to items broke after re-importing
The assets were deleted and recreated, so they got new GUIDs. Use the update-in-place pattern above and never delete the output folder to “start fresh”.
“The type or namespace name ‘UnityEditor’ could not be found” when building
The importer is in a runtime folder. Move it into an Editor folder or an Editor-only assembly.
New assets don’t appear in the Project window
The importer didn’t call AssetDatabase.SaveAssets(), or the output folder was created without a refresh. Run the menu item again and check the Console for warnings.
FAQ
Can AI write Unity .asset files directly?
It can try, but the YAML needs the script’s GUID and exact field layout. Generating JSON and importing it with an editor script is far more reliable.
Should I use JsonUtility or Newtonsoft JSON?
JsonUtility is built in and fine for flat data like the example. If you need dictionaries, top-level arrays or polymorphic data, the Newtonsoft Json package handles those.
Is AI-generated game content good enough to ship?
As a first draft, often yes for stats and structure. Names, descriptions and dialogue usually need a human editing pass to sound like your game.
Can I use a spreadsheet instead of JSON?
Yes. Export CSV and parse it in the importer, or ask the AI to convert your spreadsheet into the JSON shape. Many designers prefer editing numbers in a spreadsheet.
How do I keep game data out of reach of cheaters?
ScriptableObjects are built into your game files, like other assets. For single-player games that’s fine. For competitive multiplayer, important values should live on the server.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.




Leave a Reply