Quick answer: Separate source assets (in git-lfs) from exported assets (in project repo), generate a SHA-256 content hash manifest for every build, and tag each release with a semver version + locked manifest. Rollbacks become restoring the previous manifest and re-downloading anything that differs.
Six months into production, your project has 5,000 assets, 12 people editing them, and three regressions traced back to “I updated the texture but did not realize it affected the boss.” Game assets are code and need the same level of version discipline — but most indie projects treat them as a junk drawer. Here is a simple, practical system that scales from two people to a hundred without becoming an operations headache.
Why Ad-Hoc Versioning Breaks
Most small teams start with everything in git and the assumption that git history is the source of truth. This works until:
- Someone commits a 2 GB Blender file and the repo becomes impossible to clone.
- Someone updates an export but forgets to regenerate it, leaving the repo in an inconsistent state.
- A player reports a bug that disappears after a rebuild because an asset was silently re-exported with different parameters.
- You need to roll back one texture without reverting ten commits of unrelated changes.
Each of these is solvable but painful. Designing for them upfront is much cheaper than patching around them later.
Principle 1: Separate Source from Export
Every asset has two forms: the source (Blender file, Photoshop file, raw WAV) and the export (FBX, PNG, OGG). They should live in different places.
Source: git-lfs, an asset server (Perforce, Plastic), or a dedicated repo. Big files, few people editing. Long-lived history.
Export: your main game project repo. Small to medium files, every dev needs them, fast clones. Generated by a deterministic build step.
The rule: export files are the output of source files. Never edit an export directly. Never commit a source file to the project repo. The two locations are strictly ordered: sources feed exports.
Principle 2: Deterministic Exports
If two developers run the export pipeline on the same source file, they should get byte-identical exports. This is harder than it sounds because most export tools include timestamps, machine names, or non-deterministic sort orders. Configure them to strip this or use post-processing to canonicalize the output.
# Example Blender export script with canonicalization
bpy.ops.export_scene.fbx(
filepath=export_path,
use_metadata=False, # no timestamps
bake_anim=True,
bake_space_transform=True,
apply_scale_options='FBX_SCALE_ALL',
)
# Strip trailing bytes that vary run-to-run
canonicalize_fbx(export_path)
Deterministic exports mean that git diff on export files shows only real changes, and CI rebuilds produce the same bytes as local rebuilds.
Principle 3: Content Hash Manifests
For every build, generate a manifest that lists every asset and its SHA-256 hash. The manifest is a single small file (JSON or TSV) that summarizes the entire asset state.
{
"version": "1.2.0",
"build": "abc123",
"assets": {
"textures/hero_diffuse.png": "sha256:9c4f...",
"models/hero.fbx": "sha256:21a8...",
"audio/theme.ogg": "sha256:7bda...",
"data/weapons.json": "sha256:cd37..."
}
}
The manifest is checked into the project repo. Every release tag has a corresponding manifest. The manifest file is small (a few KB for a large game) and diffs cleanly in git.
Use the manifest for:
- Rollback. Restore the previous manifest and re-download assets that differ. Most do not.
- Delta updates. Ship only assets whose hash changed since the last version.
- Integrity checks. On startup, verify the shipped assets match the manifest.
- Debugging. “When did this asset change?” becomes a git blame on the manifest.
Principle 4: Semver for the Whole Project
Tag releases with semver: MAJOR.MINOR.PATCH. Major bumps for breaking changes to save format or asset schema. Minor bumps for new content. Patch bumps for bug fixes.
Each tag has:
- A git tag
- A frozen manifest file
- A changelog entry
- A release artifact (signed build)
Players see the semver version in the game’s about screen. Bug reports include it automatically. QA tests against specific versions. Rollbacks target specific versions. All of this flows from the single discipline of tagging.
Principle 5: Cache Assets by Hash
When an asset is uploaded to your CDN or storage, name it after its hash, not its path:
# Instead of
cdn.example.com/v1.2.0/textures/hero_diffuse.png
# Use
cdn.example.com/assets/9c4f21a8bd...png
Hash-named assets are permanently cacheable. Two releases sharing an unchanged asset reuse the same URL, and the CDN does not re-upload. Rollbacks do not re-download assets that are identical across versions.
Keep a separate mapping file (the manifest) that tells the game which hash corresponds to which logical path.
The Build Pipeline
Tying it all together, your build should:
- Checkout source at a specific git commit.
- Run the deterministic export pipeline on any source files that changed.
- Hash every export file.
- Write the manifest.
- Upload new assets to the hash-keyed store.
- Tag the release with semver and commit the manifest.
The whole pipeline should be scriptable and CI-friendly. A local developer should be able to run it with one command and get the same result as CI.
Verifying the System
Test your rollback path. Pick an old version, load its manifest, and run the game with that version’s assets. Everything should work. If anything is missing or broken, your manifest is incomplete or your storage has evicted an old hash — fix before the next release.
“Assets are code. Version them, hash them, tag them, and you can roll back bugs without reverting gameplay work. Skip the discipline and every asset change is a potential regression.”
Related Resources
For broader asset workflow, see version control strategies for game assets. For catching regressions after asset changes, see regression testing after game patches. For build testing, see how to set up automated build testing for games.
Even a one-person team benefits from a content hash manifest. The day you need to debug “why does this look different now” you will thank yourself for writing the hash down.