Skip to content

abmarnie/godot-architecture-organization-advice

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Game Project Architecture and Organization Advice for Godot 4.0+

This article summarizes my opinions on structuring a mid-sized Godot 4.0+ project. It is drawn from my recent development experience, analyzing other Godot projects, reading the practices recommended by the Godot manual, and having discussions with other experienced devs (WaterMuseum_, stimmel, Chickensoft Games, and general community feedback). I am a fan of the book "A Philosophy of Software Design" by J. Ousterhout. For comments, clarifications, or suggestions, raise an issue or start a discussion.

Disclaimer

Architecture should always be tailored to the individual needs of a project. Selectively follow the advice that makes sense to you. Avoid anything that seems unfun (don't feel compelled to do a massive refactor). Generally be judicious.

  • In a 2 day game jam, you should probably ignore architecture entirely. The importance of architecture scales with the size and complexity of your project.
  • In some projects, a more "data oriented" approach where folders are seperated by data type (file extension) might make more sense. One advantage of this approach is that you don't have to spend any time thinking about where to add new files.
  • Placing source code files further away from scenes probably won't confer you any benefits if you don't use an IDE. In fact, it may do nothing but hinder you. Even if you do use an IDE, it is rather subjective.

Contents

Directory Structure Advice

This section (mostly) aligns with the Best Practices for Project Organization section of the Godot manual. To avoid technical issues related to case sensitivity, note the use of snake_case for file and folder names, except for .cs files, which use PascalCase instead.

  • Addons Folder: Store third-party assets, scenes, and code in addons/, including their licenses.
  • Source Code Folder: Place all source code in a src/ folder for easy IDE navigation. If you don't use an IDE, this actually probably just makes things more tedious for you, in which case, treat source code like any other resource (see the "Scene-Based Assets Folder" tip below).
  • Search-Based Navigation: Prefix names of resources exclusively-used by a specific scene with that scene's name for efficient searching. Example: searching "balls_fish" should locate balls_fish.tscn and it's exclusive resources and files balls_fish.gltf, balls_fish_albedo.png, balls_fish_fishdata.tres, balls_fish.mesh, etc.
  • Scene-Based Assets Folder: Organize scenes and their resources in an assets/ folder. Each scene should have it's own folder which contains itself and it's exclusive resources.
    • Inherited Scene Folders: Nest folders for inherited scenes within their base scene's folder.
    • Locally Shared Resources: Store shared resources for a specific "scene type" in a central folder named after that "scene type", with subfolders for each owning scene. Avoid excessive nesting though.
    • Globally Shared Resources: Place general resources used by many different "scene types" in a sibling folder named after that resource's data type. For example, put all globally used .shader files in a shaders/ folder.

Here is an ASCII art example:

project_root/
|-- .gitignore
|-- .gitattributes
|-- README.md
|-- addons/
|   |-- third_party_asset_1/
|   |   |-- license.md
|   |-- third_party_asset_2/
|   |   |-- license.md
|   |-- ...
|-- assets/
|   |-- foliage/
|   |   |-- foliage.material
|   |   |-- foliage_albedo.png
|   |   |-- foliage.shader
|   |   |-- grass_1/
|   |   |   |-- grass_1.gltf
|   |   |   |-- grass_1.mesh
|   |   |   |-- grass_1.tscn
|   |   |-- grass_2/
|   |   |   |-- grass_2.gltf
|   |   |   |-- grass_2.mesh
|   |   |   |-- grass_2.tscn
|   |   |-- ...
|   |-- shaders/
|   |   |-- generic_shader_1.shader
|   |   |-- generic_shader_2.shader
|   |-- player/
|   |   |-- player.tscn
|   |   |-- player.gltf
|   |   |-- player_albedo.png
|   |-- weapon/
|   |   |-- weapon.tscn
|   |   |-- axe/
|   |   |   |-- axe_weapondata.tres
|   |   |   |-- axe.tscn
|   |   |   |-- axe.gltf
|   |   |   |-- axe_abledo.png
|   |   |-- ...
|   |-- ...
|-- src/ # Alternatively, localize the source code to the scene it controls.
|   |-- Player.cs
|   |-- Weapon.cs
|   |-- weapon_data.gd
|   |-- ...

[Back to top.]

Scene Structure Advice

  • Single Controller Script Per Scene: Attach one main "controller" script to each scene's root node. Name both the controller script and the root node after the scene. This reduces complexity by minimizing unnecessary inter-script communication. Multiple scripts may exist in the SceneTree, but only if a one-to-one correspondence between scripts and (sub)scenes is maintained.
  • Self-Contained Scenes: Scenes should strive to be self contained, possessing all necessary resources they require. The controller script attached to the root node should only directly reference their children (or descendants); otherwise dependencies need to be externally injected (see next tip). This keeps things modular and loosely coupled.
  • Dependency Injection Techniques: For scenes with external dependencies, implement dependency injection (ideally from an ancestor) in one of the following ways:
    • Scene controller emits a signal/event for external procedures to run in response to.
    • Scene controller has a public (non-underscore-prefixed) method for externals to directly call.
    • Scene controller has a publically settable (non-underscore-prefixed) field/property for externals to directly inject a reference or value into.
  • Limit Scene Inheritance: Use scene inheritance sparingly due to its inflexibility. Limit inheritance to one layer if it's too convenient to pass up. It is most useful and necessary when inherting from an imported scene (from a .blend or a .gltf file).
  • Non-Editable Subscene Children: Keep subscene children non-editable for encapsulation. Exceptions can be made (e.g., for editing collision shapes), but a design requiring editable children generally indicates that the scene's root node controller script is insufficiently exposing data or functionality.
  • Featureful Scenes: To reduce clutter, avoid creating scenes with merely 1 or 2 nodes, unless they have featureful controller scripts. Often, non-featureful scenes can just be recreated in a few clicks. Some exceptions might naturally be made for editing convenience, e.g., for reusable visuals, reusable static level props (see next tip), or if extending a class with a few pieces of data would seriously help you.
  • Generality of Scenes: Design scenes for potential reuse across the game. To reduce clutter, scenes used precisely once and which do not persist across scene loads should be "inlined" (right click in SceneTree -> Make Local -> right click in FileSystem -> View Owners to double check -> delete in FileSystem). You can always undo this later by doing: right click in SceneTree -> Save Branch as Scene.
  • Data Persistence and Sharing: Use the static keyword for scene-persistent data (if possible, e.g., you are trying to persist data for something like a Player or GUI object in a singleplayer game) or shared information and functionality. Other options include custom resources (if you need instance-specific scene-persistent without having to keep track of instance IDs) and autoloads (which the Godot docs recommends you use sparingly in large projects; as a rule of thumb, they are fine if they have no dependencies and their internal state is publically read-only).
  • Structure SceneTree by Logical Relationship: Organize the SceneTree relationally rather than spatially. Set top_level = true for spatial decoupling in parent-child relationships if needed.
  • Script Member Ordering: The more consistent things are ordered, the easier it is to navigate and make changes. It is officially recommended to order script members in the following way:
01. @tool
02. class_name (PascalCase)
03. extends
04. # docstring

05. signals (snake_case)
06. enums (PascalCase, members are CONSTANT_CASE)
07. constants (CONSTANT_CASE)
08. @export variables (snake_case)
09. public variables (non-underscore-prefixed snake_case)
10. private variables (underscore-prefixed _snake_case)
11. @onready variables (snake_case)

12. optional built-in virtual _init method
13. optional built-in virtual _enter_tree() method
14. built-in virtual _ready method
15. remaining built-in virtual methods (underscore-prefixed _snake_case)
16. public methods (non-underscore-prefixed snake_case)
17. private methods (underscore-prefixed _snake_case)
18. subclasses (PascalCase)

The same ordering rules can be applied in C#. Some C# specific ordering considerations include:

  • Put lightweight nested struct declarations up at the top, next to nested enum declarations.
  • Put the backing fields of properties right before the property which uses them, even if they would be placed somewhere else otherwise.
  • C# events (usually Action or Func types) should be placed at the top, where GDScript signals would go.
  • Get-only properties are basically just methods with non-void return types, so they should be grouped with methods.
  • Group interface implementations together. They should be placed right above public methods. You should probably make a comment /// <see cref="IMyInterface"/> to indicate the group.

[Back to top.]

Quality of Life Advice

  • Get Node Reference Sanely: Use the new scene unique nodes feature to get nodes in a non-fragile way. Using @export is fine too, especially on smaller teams.
  • Sharing Scenes across Projects: Right click on a scene, click Edit Dependencies. If the dependencies are local to that Scene's folder, then you can simply drag and drop that folder across Godot projects and things should just work. This opens up new workflows, allowing artists who aren't comfortable with Git to work in seperate / local projects.
  • Eager Assertions: Proactively assert (e.g., in _ready, or upon dependency injection) to ensure that critical node properties are correctly set. A proactive approach helps catch bugs early, reducing the need for excessive safety checks elsewhere.
  • Reduce Git Bloat: For optimal Git LFS setup and to avoid version control bloat, use the .gitattributes and .gitignore provided in this repo. Simply download them and place them in the root of your Git repo.
  • Refactor in the Editor: Always move or rename files within the Godot editor to avoid Godot's cache from being desynchronized with your local files. If you need to rename an entire folder and doing so naively breaks things (you're using Git, right?), consider first renaming the leaf files before recursively working your way down to the root folder.
  • Commit Frequently While Refactoring: As of Godot 4.2, refactoring (renaming and moving around files) is still not very stable. Before you attempt a file or folder rename, or moving files, make a commit, so you can git restore . in case things go bad.
  • Importing 3D Assets:.gltf is generally recommended for larger teams (.gltf is to be preferred over .glb). For small teams in which everyone is comfortable having Blender installed, working directly with .blend files in the engine is extremely convenient for fast iteration, and is also more convenient for version control purposes (so long as you use the .gitattributes and .gitignore file provided in this repo). See this for detailed instructions for working with .blend files in Godot.
  • Prefer .tres for Git: When working with resources, prefer .tres over .res file extension, except potentially when dealing with large numerical data blobs like meshes. This makes Git history more human-interpretable.
  • Resource File Extension Consistency: Be consistent with resource file extensions. For example, if you want to use .mesh over .res/.tres for mesh data, do so consistently. The same goes for .material, .shape, etc.
  • Node Utilization: Leverage existing Nodes for common functionalities, unless you have a good reason to roll your own.
  • View Owners before Deleting: Right click -> View Owners before deleting scenes or resources, to make sure you won't break anything.
  • Reduce FileSystem Clutter: Create an empty .gdignore file in any folders which shouldn't show up inside the FileSystem dock.
  • Improve Folder Visibility: Color code project folders with right click -> Set Folder Color.
  • Font Size: Increase font size in Editor Settings to reduce long-term eye strain.
  • Tech Stack Updates: Regularly update Godot, .NET/C#, etc., for improvements. Be cautious about updating near release, or once your project becomes very large.
  • Static Type Warnings: As of Godot 4.2+, enable type warnings for better autocomplete and compile time error detection. If you prefer succinctness, use type inference syntax := for variable initialization.
  • Script Templates: Consider saving the default.gd (or Default.cs) file provided in this repo into a .gdignored script_templates/node folder. Doing so provides you with a nicer default starting template everytime you create a new script.

[Back to top.]

About

Advice for architecting and organizing Godot projects.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published