Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Game Development with Godot & GDScript  Module 1 – Godot, the editor and your first scene

Project files, UIDs and version control

Know where res://, user:// and uid:// point, commit the right Godot files, keep references working with .uid files, and set up Git, LFS and scene merges.

  • Beginner
  • 15 minutes
  • Examples run with godot 4.7.2-stable and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain where res://, user:// and uid:// paths point, and which of them a game can write to
  • Decide which project files to commit and which to ignore, and rebuild .godot/ after a clone
  • Move files without breaking references by keeping their .uid and .import files with them
  • Set up Git for a Godot project with .gitignore, .gitattributes and Git LFS for large assets
  • Read a scene diff, resolve a conflict in a .tscn file and check that the scene still loads

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain where res://, user:// and uid:// paths point, and which of them a game can write to
  • Decide which project files to commit and which to ignore, and rebuild .godot/ after a clone
  • Move files without breaking references by keeping their .uid and .import files with them
  • Set up Git for a Godot project with .gitignore, .gitattributes and Git LFS for large assets
  • Read a scene diff, resolve a conflict in a .tscn file and check that the scene still loads

Before you start

On this page

A teammate moves one texture into a new folder outside the editor, commits, and the next morning your copy of the game shows the coin with no picture. Three files decide whether that happens: the texture, its .import file and the scene that refers to it, and only two of the three made it into the commit.

Here you learn where Godot’s res://, user:// and uid:// paths point, which files of a project belong in version control, how UIDs keep references working when files move, how to set up Git with the files Godot itself writes, and how to read a scene diff and resolve a conflict in one.

res:// is the project, user:// is the player’s, uid:// survives moves

Every path in Godot starts with one of three prefixes, and each points somewhere different:

Path Points to Can the game write there?
res:// the project’s folder, the one that holds project.godot only when run from the editor; an exported game’s files are read-only
user:// a folder per project and user, outside the project always: saves, settings and logs go here
uid:// the file that owns a unique id, wherever it is now a reference, not a place to write

res://: The path prefix for the files of a project: res:// is the folder that holds project.godot. It can be written while the project runs from the editor, and is read-only in an exported game. is the project as you see it in the FileSystem dock. Once the game is exported, its files are packed and read-only, so anything a game saves goes to user://: The path prefix for a game's writable data, such as saves, settings and logs, kept per project and user outside the project. On macOS it is ~/Library/Application Support/Godot/app_userdata/ and the project's name. instead. Godot’s documentation gives user:// on Windows as %APPDATA%\Godot\app_userdata\[project_name], on macOS as ~/Library/Application Support/Godot/app_userdata/[project_name], and on Linux as ~/.local/share/godot/app_userdata/[project_name], where the folder name comes from the project’s name. This script asks the engine for both folders and for the third kind of path, a UID:

paths_report.gd: where the three kinds of path point GD · dodge/paths_report.gd
extends SceneTree

# Where res://, user:// and uid:// point on this computer.
func _initialize() -> void:
	print("res:// is  ", ProjectSettings.globalize_path("res://"))
	print("user:// is ", ProjectSettings.globalize_path("user://"))
	var id := ResourceLoader.get_resource_uid("res://art/coin.svg")
	print("res://art/coin.svg has the UID ", ResourceUID.id_to_text(id))
	print("and that UID leads to ", ResourceUID.get_id_path(id))
	var texture := load("uid://bf5btoh7rd06h") as Texture2D
	print("Loaded by UID: a ", texture.get_class(), " of ", texture.get_size())
	var file := FileAccess.open("user://best_score.txt", FileAccess.WRITE)
	file.store_string("1200")
	file.close()
	print("user://best_score.txt saved: ", FileAccess.file_exists("user://best_score.txt"))
	quit()
res://, user:// and a UID on macOS, Godot 4.7.2 Python · paths_session.py
from session import files, run

files("dodge=.")
run("""
godot --headless --quiet --import
godot --headless --no-header --script paths_report.gd
ls ~/Library/Application\\ Support/Godot/app_userdata/Dodge
""")

Output

$ godot --headless --quiet --import
$ godot --headless --no-header --script paths_report.gd
res:// is  ~/game/
user:// is ~/Library/Application Support/Godot/app_userdata/Dodge/
res://art/coin.svg has the UID uid://bf5btoh7rd06h
and that UID leads to res://art/coin.svg
Loaded by UID: a CompressedTexture2D of (16.0, 16.0)
user://best_score.txt saved: true
$ ls ~/Library/Application\ Support/Godot/app_userdata/Dodge
best_score.txt
logs
objectdb_snapshots

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 paths_session.py

The project named Dodge has its user:// in app_userdata/Dodge, and the listing shows the saved best_score.txt there beside folders that Godot writes itself, such as logs. The texture res://art/coin.svg has the UID: A unique id that Godot gives every file, written as uid:// and a code. References store it beside the path, so they keep working when the file moves together with its .uid or .import file. uid://bf5btoh7rd06h, and load("uid://bf5btoh7rd06h") loads it without naming any folder. That is what makes UIDs useful: the next sections show how they survive a move.

Renaming the project and losing its saves

user:// is named after application/config/name. Rename the project and the game looks in a new, empty folder, so saves from before seem lost. Set a fixed folder name with application/config/use_custom_user_dir and custom_user_dir_name before the first release.

Checkpoint

Check: res://, user:// and uid:// paths

  1. Question 1 of 2 Where should a game write its save file?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: user://

    Saves, settings and logs belong in user://, a folder per project and user, which the run on macOS showed as ~/Library/Application Support/Godot/app_userdata/Dodge.

  2. Question 2 of 2 A game saves its options to res://options.cfg. It works when run from the editor. What happens in the exported game?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The write fails, because an exported game's files are read-only

    Running from the editor hides the bug, because the project folder is writable there. Write user://options.cfg instead, the same path in the editor and in every export.

Commit the project and its metadata, never the .godot/ cache

A Godot project is mostly text: scenes, resources, scripts and project.godot. Beside some files Godot writes metadata files that must travel with them, and inside the project it keeps a cache that must not travel at all:

File What it is Commit it?
project.godot, .tscn, .tres, .gd, assets the project itself yes
*.import beside an asset its import settings and its UID yes, with the asset
*.uid beside a script or shader its UID (since 4.4) yes, with the file
export_presets.cfg export settings; 4.1 and later keep credentials elsewhere yes
.godot/ folder: A project's cache: imported copies of its assets, the list of global classes and the UID cache. Godot rebuilds it from the project's files with one import, so it is never committed. the cache: imported copies, the class list, the UID cache never
*.translation binary translations imported from CSV files never
exported builds the output of an export never; publish them instead

The cache can always be rebuilt from the committed files, which is exactly why it stays out of the repository. A fresh clone therefore has no .godot/ folder, and that matters the first time you run a script without opening the editor. Step through it:

Interactive · message by message From a fresh clone to a project that runs Step through the clone of the Dodge project. The sub-label under Your folder says what it holds at each step.
From a fresh clone to a project that runs, step 5 of 5. Messages so far: Repository to Your folder: committed files; Your folder to Godot 4.7.2: run uses_player.gd; Godot 4.7.2 to Your folder: writes .godot/; Your folder to Godot 4.7.2: run uses_player.gd.Repositorywhat was committedYour folder.godot/ rebuiltGodot 4.7.2committed filesrun uses_player.gdwrites .godot/run uses_player.gd

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Clone. The clone brings every committed file, scripts, scenes, assets and their .uid and .import files, but no .godot/ folder, because .gitignore keeps the cache out of the repository.
  2. A script fails. uses_player.gd uses the class Player that player.gd declares with class_name. Run headless now, it fails to parse, because the list of global classes lives in the cache that does not exist yet.
  3. Import once. godot --headless --import scans the project as the editor does when it first opens it, imports the assets, registers the global classes and the UIDs, and quits.
  4. The cache is back. .godot/ now holds global_script_class_cache.cfg, uid_cache.bin and the imported assets. All of it can be rebuilt from the committed files, which is why it is never committed.
  5. The script runs. The same command now prints "A Player with 3 lives". Opening the project in the editor does the same import, so this step matters for build machines and scripts that never open the editor.

Here is that clone of the Dodge project, run for real:

A fresh clone before and after its first import, Godot 4.7.2 Python · clone_session.py
from session import files, run

# The project as a fresh clone has it: scripts, scenes, assets and their .uid and .import files, but no .godot/ folder.
files("dodge=.")
run("""
ls -A
!godot --headless --no-header --script uses_player.gd
godot --headless --quiet --import
ls -A .godot
godot --headless --no-header --script uses_player.gd
""")

Output

$ ls -A
art
check_coin.gd
check_coin.gd.uid
check_level.gd
check_level.gd.uid
coin.gd
coin.gd.uid
coin.tscn
paths_report.gd
paths_report.gd.uid
player.gd
player.gd.uid
project.godot
uses_player.gd
uses_player.gd.uid
$ godot --headless --no-header --script uses_player.gd
SCRIPT ERROR: Parse Error: Identifier "Player" not declared in the current scope.
          at: GDScript::reload (res://uses_player.gd:5)
SCRIPT ERROR: Parse Error: Cannot infer the type of "player" variable because the value doesn't have a set type.
          at: GDScript::reload (res://uses_player.gd:5)
ERROR: Failed to load script "res://uses_player.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --quiet --import
$ ls -A .godot
.gdignore
editor
global_script_class_cache.cfg
imported
uid_cache.bin
$ godot --headless --no-header --script uses_player.gd
A Player with 3 lives

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 clone_session.py

Before the import, Player, a class that player.gd declares with class_name, was unknown, because the list of global classes lives in .godot/global_script_class_cache.cfg. One godot --headless --import rebuilt the whole cache, and the same script then ran. Opening the project in the editor does the same import, so this step is for build machines and scripts that never open the editor.

Checkpoint

Check: What to commit and what to ignore

  1. Question 1 of 2 Which of these files belong in the repository? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • player.gd.uid
    • export_presets.cfg
    • art/coin.svg.import

    Commit the project and the metadata beside its files; ignore the .godot/ cache. The git session listed what the first commit will hold: every script with its .uid, the texture with its .import, and nothing from .godot/.

  2. Question 2 of 2 A build machine clones the project and runs godot --headless --script uses_player.gd. It fails with: Identifier "Player" not declared. What is missing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An import first, which builds the .godot/ cache with the list of global classes

    The clone session showed the same error, then ran godot --headless --import, and the script printed "A Player with 3 lives". Build scripts should run the import first.

A UID keeps a reference working when its file moves

Every scene, resource, script, shader and imported asset of a project has a UID. A script or a shader keeps its UID in a .uid file beside it; an imported asset keeps it in its .import file: The file Godot writes beside each imported asset, such as coin.svg.import, with the asset's import settings and its UID. It belongs in version control with the asset.; a scene or a resource keeps it in its own first line. A scene stores both the UID and the path of each file it uses, and the UID wins.

New in Godot 4.4 and 4.5

Godot 4.4 gave UIDs to the file types that had none, with the .uid files beside scripts and shaders, and a UID upgrade tool for projects from 4.3. Since 4.5, holding Ctrl (Cmd on macOS) while dropping a file into a script writes a preload() by UID.

Step through how coin.tscn finds its texture after two moves:

Interactive · step by step How coin.tscn finds coin.svg after a move Step through two moves of the texture. Watch where the UID of the file is kept and what the scene's line points to.
How coin.tscn finds coin.svg after a move, step 5 of 5. Showing coin.tscn, UID cache (…rd06h is unknown), coin.svg (in icons/), coin.svg.import (a new UID). In focus: coin.tscn, coin.svg.look up the UIDpath of the UIDcoin.tscnuid …rd06h, path art/UID cache…rd06h is unknowncoin.svgin icons/coin.svg.importa new UIDtexture missing

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The scene names both. coin.tscn refers to its texture with an ext_resource line that holds a UID, uid://bf5btoh7rd06h, and a path, res://art/coin.svg. The UID is the main reference; the path is a fallback.
  2. The UID lives in .import. An imported file keeps its UID in its .import file. When Godot scans the project, it records every UID and the file it belongs to in the UID cache.
  3. Loading by UID. To load the scene, Godot looks up the UID in the cache and loads the file it names, res://art/coin.svg, the first run of check_coin.gd.
  4. Moved with its .import. The texture moves to sprites/ together with coin.svg.import. The next scan finds the same UID at the new place, so the scene loads res://sprites/coin.svg although its own line still says art/.
  5. Moved without it. Moved again with its .import file deleted, the texture gets a new random UID at the next scan. The scene's UID is unknown, Godot warns and tries the path, and art/coin.svg is gone, so the texture is missing.

Both moves, run for real: the first carries the metadata files along, the second leaves them behind, as a file manager or a careless commit would:

Two moves of a texture and a script, Godot 4.7.2 Python · move_session.py
from session import files, run

files("dodge=.")
run("""
godot --headless --quiet --import
godot --headless --no-header --script check_coin.gd
# Move the texture and the script with their metadata files, as the FileSystem dock does.
mkdir sprites scripts
mv art/coin.svg art/coin.svg.import sprites/
mv coin.gd coin.gd.uid scripts/
godot --headless --quiet --import
godot --headless --no-header --script check_coin.gd
grep ext_resource coin.tscn
# Move them again, this time leaving the metadata files behind.
mkdir icons
mv sprites/coin.svg icons/
mv scripts/coin.gd coin_logic.gd
rm sprites/coin.svg.import scripts/coin.gd.uid
godot --headless --quiet --import
!godot --headless --no-header --script check_coin.gd
""")

Output

$ godot --headless --quiet --import
$ godot --headless --no-header --script check_coin.gd
Texture: res://art/coin.svg
Script: res://coin.gd
# Move the texture and the script with their metadata files, as the FileSystem dock does.
$ mkdir sprites scripts
$ mv art/coin.svg art/coin.svg.import sprites/
$ mv coin.gd coin.gd.uid scripts/
$ godot --headless --quiet --import
$ godot --headless --no-header --script check_coin.gd
Texture: res://sprites/coin.svg
Script: res://scripts/coin.gd
$ grep ext_resource coin.tscn
[ext_resource type="Texture2D" uid="uid://bf5btoh7rd06h" path="res://art/coin.svg" id="1_tex"]
[ext_resource type="Script" uid="uid://cmoaxedjfbo8h" path="res://coin.gd" id="2_script"]
# Move them again, this time leaving the metadata files behind.
$ mkdir icons
$ mv sprites/coin.svg icons/
$ mv scripts/coin.gd coin_logic.gd
$ rm sprites/coin.svg.import scripts/coin.gd.uid
$ godot --headless --quiet --import
$ godot --headless --no-header --script check_coin.gd
WARNING: res://coin.tscn:3 - ext_resource, invalid UID: uid://bf5btoh7rd06h - using text path instead: res://art/coin.svg
     at: load (scene/resources/resource_format_text.cpp:501)
     GDScript backtrace (most recent call first):
         [0] _initialize (res://check_coin.gd:5)
ERROR: Resource file not found: res://art/coin.svg (expected type: Texture2D)
   at: _load (core/io/resource_loader.cpp:325)
   GDScript backtrace (most recent call first):
       [0] _initialize (res://check_coin.gd:5)
WARNING: res://coin.tscn:4 - ext_resource, invalid UID: uid://cmoaxedjfbo8h - using text path instead: res://coin.gd
     at: load (scene/resources/resource_format_text.cpp:501)
     GDScript backtrace (most recent call first):
         [0] _initialize (res://check_coin.gd:5)
ERROR: Attempt to open script 'res://coin.gd' resulted in error 'File not found'.
   at: load_source_code (modules/gdscript/gdscript.cpp:1139)
   GDScript backtrace (most recent call first):
       [0] _initialize (res://check_coin.gd:5)
ERROR: Failed loading resource: res://coin.gd.
   at: _load (core/io/resource_loader.cpp:317)
   GDScript backtrace (most recent call first):
       [0] _initialize (res://check_coin.gd:5)
ERROR: res://coin.tscn:7 - Parse Error: [ext_resource] referenced non-existent resource at: res://art/coin.svg.
   at: _printerr (scene/resources/resource_format_text.cpp:41)
   GDScript backtrace (most recent call first):
       [0] _initialize (res://check_coin.gd:5)
ERROR: res://coin.tscn:8 - Parse Error: [ext_resource] referenced non-existent resource at: res://coin.gd.
   at: _printerr (scene/resources/resource_format_text.cpp:41)
   GDScript backtrace (most recent call first):
       [0] _initialize (res://check_coin.gd:5)
Texture: missing
Script: missing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 move_session.py

After the first move, coin.tscn still says art/coin.svg and coin.gd, yet the texture and the script loaded from sprites/ and scripts/, found by their UIDs. After the second, the old UIDs had gone with the deleted metadata files: Godot warned “invalid UID … using text path instead”, the old paths led nowhere, and the coin had no texture and no script.

The FileSystem dock moves the metadata files for you and updates the scenes that refer to a file. Godot’s documentation asks you to move, rename and delete assets there, and when a file must move outside the editor, to move its .uid file too.

Key idea

A file’s UID lives in its .uid or .import file, so commit those files and move them with their file: then every reference survives the move, even one whose path is out of date.

Checkpoint

Check: UIDs and moving files

  1. Question 1 of 2 You move coin.svg and coin.svg.import to sprites/ in a file manager; coin.tscn still says path="res://art/coin.svg". What happens when the scene loads after the next scan?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The texture loads from sprites/, found by its UID

    In the session the first move kept both metadata files with their files, and check_coin.gd found the texture in sprites/ and the script in scripts/ while coin.tscn still named the old paths.

  2. Question 2 of 2 Where does Godot 4.7 keep the UID of the script coin.gd?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In coin.gd.uid, beside the script

    An imported asset keeps its UID in its .import file, a scene or resource in its first line, and a script or a shader in a .uid file beside it.

Two small files set Git up for a Godot project

When you choose Git in the New Project dialog, or Project > Version Control > Generate Version Control Metadata later, Godot writes two files. Its 4.7.2 source writes exactly these lines: a .gitignore with .godot/ and /android/, the folder the Android build template is installed into, and a .gitattributes with * text=auto eol=lf, which stores text files with LF line endings on every system. A real project adds a few lines, as this session does: the binary translations, a folder for exported builds, and Git LFS: An extension of Git that stores large binary files, such as textures and sounds, on a separate server and keeps small text pointers to them in the repository. Each machine installs it once. for large binary assets:

Git set up for a Godot project, Git 2.54 and Godot 4.7.2 Python · git_session.py
from session import files, run, write

files("dodge=.")
# The two files of the Project Manager's Git option, each with a few lines added for a real project.
write(".gitignore", """# Godot 4+ specific ignores
.godot/
/android/

# Binary translations Godot imports from CSV files
*.translation

# Exported builds
/builds/
""")
write(".gitattributes", """# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

# Large binary assets go to Git LFS
*.png filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.glb filter=lfs diff=lfs merge=lfs -text
""")
run("""
git --version
godot --headless --quiet --import
mkdir builds && touch builds/dodge.x86_64
git init -q
git check-ignore -v .godot/uid_cache.bin builds/dodge.x86_64
!git check-ignore -v player.gd.uid art/coin.svg.import
git check-attr filter eol -- art/coin.svg art/coin.png sounds/jump.wav
git add -A
git status --short
""")

Output

$ git --version
git version 2.54.0 (Apple Git-157)
$ godot --headless --quiet --import
$ mkdir builds && touch builds/dodge.x86_64
$ git init -q
$ git check-ignore -v .godot/uid_cache.bin builds/dodge.x86_64
.gitignore:2:.godot/	.godot/uid_cache.bin
.gitignore:9:/builds/	builds/dodge.x86_64
$ git check-ignore -v player.gd.uid art/coin.svg.import
[exit status 1]
$ git check-attr filter eol -- art/coin.svg art/coin.png sounds/jump.wav
art/coin.svg: filter: unspecified
art/coin.svg: eol: lf
art/coin.png: filter: lfs
art/coin.png: eol: lf
sounds/jump.wav: filter: lfs
sounds/jump.wav: eol: lf
$ git add -A
$ git status --short
A  .gitattributes
A  .gitignore
A  art/coin.svg
A  art/coin.svg.import
A  check_coin.gd
A  check_coin.gd.uid
A  check_level.gd
A  check_level.gd.uid
A  coin.gd
A  coin.gd.uid
A  coin.tscn
A  paths_report.gd
A  paths_report.gd.uid
A  player.gd
A  player.gd.uid
A  project.godot
A  uses_player.gd
A  uses_player.gd.uid

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 git_session.py

git check-ignore -v names the rule that ignores each path: line 2 of .gitignore for the cache and line 9 for the build. For player.gd.uid and art/coin.svg.import it printed nothing and ended with status 1, which here means good news: neither is ignored. git check-attr shows that PNG and WAV files would go through the lfs filter, while the SVG, a text file, stays in Git with LF endings. The final git status lists what the first commit will hold: every script with its .uid, the texture with its .import, and no .godot/.

Two rules from Godot’s documentation keep LFS painless. Set it up before you commit any large file, because files committed earlier stay in the history and must be removed and added again. And everyone who clones the repository needs Git LFS installed; run git lfs install once per machine. If a project has no .gitattributes, Windows users should also set git config --global core.autocrlf input, so Git does not mark files as changed by their line endings.

Ignoring the .import or .uid files

A .gitignore line such as *.import or *.uid looks tidy and breaks every reference by UID on the next clone: each file gets a new UID there, and scenes fall back to their paths, with a warning for each, or fail when a path has changed.

Checkpoint

Check: .gitignore, .gitattributes and Git LFS

  1. Question 1 of 2 git check-ignore -v player.gd.uid prints nothing and ends with status 1. What does that mean?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The file is not ignored, so it will be committed, which is what you want

    For .godot/uid_cache.bin and builds/dodge.x86_64 the command printed the line of .gitignore that ignores each; for the .uid and .import files it printed nothing, because no rule matches them.

  2. Question 2 of 2 A team commits 2 GB of WAV files, then adds *.wav filter=lfs to .gitattributes. What does Godot's documentation say about those WAV files?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: They stay in the history as normal Git files until they are removed and added again, or migrated

    Godot's version control page says to set up Git LFS before committing any files, and that files committed earlier must be removed and re-added, or converted with git lfs migrate.

A scene change is a one-line diff, and a conflict is a choice

A scene in text form makes version control useful: move one coin in the editor, save, and the diff shows one changed line. When two branches change the same line, Git cannot choose, and the scene holds conflict markers until you do. This session moves Coin2 on two branches, right on main and up on raise-coin, merges them, and checks the scene with Godot before and after the conflict is resolved:

A scene diff, a conflict and its resolution, Git 2.54 and Godot 4.7.2 Python · conflict_session.py
from session import files, run, write

# level.tscn and three versions of it as the editor saves it after Coin2 is moved, kept outside the project in ~/edits.
files("dodge=.", "edits/level.tscn=level.tscn", "edits=../edits")
write(".gitignore", "# Godot 4+ specific ignores\n.godot/\n/android/\n")
run("""
godot --headless --quiet --import
git init -q
git add -A
git commit -q -m "Level with three coins"
git switch -q -c raise-coin
cp ~/edits/coin2_up.tscn level.tscn
git commit -q -am "Raise coin 2"
git switch -q main
cp ~/edits/coin2_right.tscn level.tscn
git diff
git commit -q -am "Move coin 2 right"
!git merge raise-coin
sed -n '9,15p' level.tscn
!godot --headless --no-header --script check_level.gd
cp ~/edits/coin2_right_and_up.tscn level.tscn
godot --headless --no-header --script check_level.gd
git add level.tscn
git commit -q -m "Merge raise-coin: coin 2 right and up"
git log --oneline --graph
""")

Output

$ godot --headless --quiet --import
$ git init -q
$ git add -A
$ git commit -q -m "Level with three coins"
$ git switch -q -c raise-coin
$ cp ~/edits/coin2_up.tscn level.tscn
$ git commit -q -am "Raise coin 2"
$ git switch -q main
$ cp ~/edits/coin2_right.tscn level.tscn
$ git diff
diff --git a/level.tscn b/level.tscn
index 280e8db..15a4f6f 100644
--- a/level.tscn
+++ b/level.tscn
@@ -7,7 +7,7 @@
 [node name="Coin1" parent="." unique_id=1628878745 instance=ExtResource("1_coin")]

 [node name="Coin2" parent="." unique_id=403667776 instance=ExtResource("1_coin")]
-position = Vector2(40, 0)
+position = Vector2(48, 0)

 [node name="Coin3" parent="." unique_id=2076929865 instance=ExtResource("1_coin")]
 position = Vector2(80, 0)
$ git commit -q -am "Move coin 2 right"
$ git merge raise-coin
Auto-merging level.tscn
CONFLICT (content): Merge conflict in level.tscn
Automatic merge failed; fix conflicts and then commit the result.
[exit status 1]
$ sed -n '9,15p' level.tscn
[node name="Coin2" parent="." unique_id=403667776 instance=ExtResource("1_coin")]
<<<<<<< HEAD
position = Vector2(48, 0)
=======
position = Vector2(40, -16)
>>>>>>> raise-coin

$ godot --headless --no-header --script check_level.gd
ERROR: Parse Error: Parse error. [Resource file res://level.tscn:12]
   at: _parse_node_tag (scene/resources/resource_format_text.cpp:293)
   GDScript backtrace (most recent call first):
       [0] _initialize (res://check_level.gd:5)
ERROR: Failed loading resource: res://level.tscn.
   at: _load (core/io/resource_loader.cpp:317)
   GDScript backtrace (most recent call first):
       [0] _initialize (res://check_level.gd:5)
level.tscn did not load
[exit status 1]
$ cp ~/edits/coin2_right_and_up.tscn level.tscn
$ godot --headless --no-header --script check_level.gd
Coin1 at (0.0, 0.0)
Coin2 at (48.0, -16.0)
Coin3 at (80.0, 0.0)
$ git add level.tscn
$ git commit -q -m "Merge raise-coin: coin 2 right and up"
$ git log --oneline --graph
*   a13c282 Merge raise-coin: coin 2 right and up
|\
| * 82e453c Raise coin 2
* | a0dbd94 Move coin 2 right
|/
* 8c45742 Level with three coins

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conflict_session.py

The diff is one line: position = Vector2(40, 0) became (48, 0). The merge stopped with a conflict on that line, and the scene then held both versions between <<<<<<<, ======= and >>>>>>>. Godot cannot load a scene in that state: the check printed a parse error at line 12, inside the conflict. The fix is a choice per property. Here the coin should move both right and up, so the resolved line is position = Vector2(48, -16), the scene loads again, and the merge commit records the choice.

Three habits keep scene conflicts rare and small. Split big levels into scenes, so that two people rarely edit the same file. Commit often, so that conflicts cover few lines. And after upgrading to 4.6 or later, save every scene once with Project > Tools > Upgrade Project Files, so the new unique_id of every node arrives in one commit instead of in everyone’s next diffs.

Text Diff Paste two versions of a .tscn file to see which properties changed, line by line.

Checkpoint

Check: Scene diffs and conflicts

  1. Question 1 of 2 A merge leaves Coin2 with <<<<<<< HEAD, position = Vector2(48, 0), =======, position = Vector2(40, -16), >>>>>>> raise-coin. Both changes were wanted. What do you write?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: One line, position = Vector2(48, -16), then load the scene in Godot before committing

    A conflict in a .tscn file is a choice per property. After writing the chosen line, the session ran check_level.gd, which loaded the scene and printed Coin2 at (48.0, -16.0).

  2. Question 2 of 2 What does Godot do with a .tscn file that still contains conflict markers?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It fails to load the scene with a parse error

    A scene with conflict markers cannot be loaded, which is why loading it after a merge, in the editor or with a headless script, is the quickest check that the resolution is complete.

Practise: set up version control for a project

Use the Pixel Dodge project of the earlier lessons, or any project of your own:

  1. Open the project’s folder in a terminal and run git init. If the project has no .gitignore and .gitattributes, create them with Project > Version Control > Generate Version Control Metadata.
  2. Add /builds/ and *.translation to .gitignore, and LFS lines for the binary types your project uses to .gitattributes, as in the session; run git lfs install if you use LFS.
  3. Run git check-ignore -v .godot/uid_cache.bin and expect line 2 of .gitignore; run it on one .uid and one .import file and expect no output and status 1.
  4. Commit, then clone the repository into a new folder, run godot --headless --import there, and open it.
  5. Move one asset to another folder in the FileSystem dock and look at git status: the asset and its .import file move together, and the scenes that use it change their paths.

Then work through the self-check below. It covers the five parts of this page, asks about the part you are least sure of first, and gives the reason for every answer straight away:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 On macOS, where is user:// for a project named Dodge?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: ~/Library/Application Support/Godot/app_userdata/Dodge

    The documentation gives one user:// folder per platform: %APPDATA%\Godot\app_userdata\[project_name] on Windows, ~/Library/Application Support/Godot/app_userdata/[project_name] on macOS and ~/.local/share/godot/app_userdata/[project_name] on Linux.

  2. Question 2 of 10 A game renamed from Dodge to Dodge Deluxe in an update. Players say their saves vanished. What happened, and how do you avoid it next time?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: user:// follows the project's name, so the update looks in a new folder; set a custom user folder name before release

    The ProjectSettings reference warns that changing application/config/name can move the user data folder. A custom user folder name keeps it fixed.

  3. Question 3 of 10 Why is the .godot/ folder never committed?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: It is a cache that Godot rebuilds from the project's files with one import

    The documentation describes .godot/ as cache data that Godot creates when it first opens a project. Committing it only adds large, machine-made files to every diff.

  4. Question 4 of 10 Which first step should a build script that runs Godot scripts from a fresh clone take?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: godot --headless --import, to build the .godot/ cache

    --import starts the editor headless, waits for the import and quits, which leaves the project ready for headless scripts and exports.

  5. Question 5 of 10 What does load("uid://bf5btoh7rd06h") load?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: The file whose UID that is, wherever it is in the project now

    A uid:// path names a file by its UID, so it keeps working when the file moves with its metadata, which is why 4.5 lets you drop a file into a script with Ctrl held to get a preload by UID.

  6. Question 6 of 10 A teammate moved ten scripts into scripts/ with a file manager and committed them without their .uid files. What do you see after pulling, and what is the fix?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: New UIDs for those scripts, warnings or missing scripts in scenes; restore the .uid files from history and commit them with the scripts

    Without its .uid file a moved script gets a new UID, so scenes see an unknown UID and fall back to the old path, which no longer exists, as the second move in the session showed.

  7. Question 7 of 10 What does the line * text=auto eol=lf in .gitattributes do?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Git stores and checks out every text file with LF line endings, on every system

    The check-attr run showed eol: lf for the files. Without such a line, Windows users would need core.autocrlf set to input to avoid files marked as changed.

  8. Question 8 of 10 A new teammate clones the repository, and the PNG textures in the project are tiny text files. What did they skip?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: Installing Git LFS and running git lfs install before cloning

    Git LFS replaces large files with text pointers inside Git and keeps their content on a server. Godot's documentation notes that every machine that clones the repository may need Git LFS installed.

  9. Question 9 of 10 After an upgrade from 4.5 to 4.7, every scene a teammate saves shows dozens of new unique_id values in its diff. What is the clean way to handle it?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: Run Project > Tools > Upgrade Project Files once and commit all scenes together

    The 4.6 release page points to Upgrade Project Files for exactly this: save every scene once in the new format, in one commit, so the ids do not show up in everyone's next diffs.

  10. Question 10 of 10 Two designers conflict in level.tscn almost every week. What change helps most?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Split the level into smaller scenes, so each person mostly edits a different file

    Text scenes make small diffs and merges possible, and splitting big scenes keeps people out of each other's files. Frequent commits keep the conflicts that remain small.

Interview questions

Warm-up: which files of a Godot project do you commit, and which do you ignore?

Commit the project: project.godot, scenes and resources (.tscn, .tres), scripts, assets, export_presets.cfg, and the metadata files Godot writes beside them, every .import file of an asset and every .uid file of a script or shader, because they hold the UIDs that references depend on. Ignore the .godot/ folder, a cache of imported copies, the list of global classes and the UID cache that Godot rebuilds with one import, and *.translation files, binary translations imported from CSV files. Exported builds stay out too. Godot’s own Git metadata writes a .gitignore with .godot/ and /android/, and Git LFS is worth setting up for large binary assets before their first commit.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A teammate reorganised the art folders and pushed. On their machine everything works; on yours, half the scenes show missing textures and Godot prints "invalid UID" warnings. What most likely happened, and how do you repair the project without redoing the reorganisation?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your four-person team gets merge conflicts in .tscn files every week, mostly in one big level scene, and twice a conflicted scene was committed and broke the build. What do you change in the project, the workflow and the checks?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • res:// is the project’s folder and read-only in an exported game, user:// is a writable folder per project and user, named after the project, and uid:// points to a file wherever it has moved.
  • Commit project.godot, scenes, scripts, assets and every .import and .uid file; never commit .godot/, which one godot --headless --import rebuilds after a clone.
  • A file’s UID lives in its .uid or .import file, so moving the file with its metadata keeps every reference working, and moving it without breaks references whose paths have changed.
  • Godot’s Git option writes a .gitignore for .godot/ and /android/ and a .gitattributes for LF line endings; add your builds, the binary translations and Git LFS for large assets before their first commit.
  • A scene change is a small text diff, a conflict is one property to choose, and Godot loading the scene afterwards is the check that the resolution worked.

Terms in this lesson

.godot/ folder
A project's cache: imported copies of its assets, the list of global classes and the UID cache. Godot rebuilds it from the project's files with one import, so it is never committed.
.import file
The file Godot writes beside each imported asset, such as coin.svg.import, with the asset's import settings and its UID. It belongs in version control with the asset.
Git LFS
An extension of Git that stores large binary files, such as textures and sounds, on a separate server and keeps small text pointers to them in the repository. Each machine installs it once.
res://
The path prefix for the files of a project: res:// is the folder that holds project.godot. It can be written while the project runs from the editor, and is read-only in an exported game.
UID
A unique id that Godot gives every file, written as uid:// and a code. References store it beside the path, so they keep working when the file moves together with its .uid or .import file.
user://
The path prefix for a game's writable data, such as saves, settings and logs, kept per project and user outside the project. On macOS it is ~/Library/Application Support/Godot/app_userdata/ and the project's name.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.