Environment Management in Godot 4: Feature Tags

Environment Management in Godot 4: Feature Tags

There is a moment in every growing studio’s lifecycle where the codebase transitions from a prototyping sandbox to a production-ready pipeline. During a recent multi-platform porting project, I reviewed a codebase where if OS.has_feature(“debug”) was scattered across dozens of core singletons. UI elements, logging systems, and network endpoints were all hardcoded with inline conditionals.

When you rely on manual conditional checks scattered throughout your scripts, you are inviting human error into your deployment process. Godot 4 provides an elegant, engine-level solution to this: Feature Tags.

Instead of asking the game to constantly check its environment at runtime, you can configure Godot to automatically inject the correct variables based on the active build environment. Furthermore, using this Feature Tags system is the perfect alternative to the typical global Constants class (or Singletons). Because it is natively integrated into the editor, you can configure and override values centrally without the need for unnecessary autoloads for static configuration parameters.

Feature Tags Overrides

Everything begins in the Project Settings. By defining a base configuration and allowing Godot to override it during specific run states, your code remains completely agnostic to the environment it’s running in.

The Workflow:

  1. Create the Base Value (Release/Production): Navigate to Project -> Project Settings. In the top search bar, type a custom property path (e.g., dev/dialogs/enabled). Set its type to bool and click Add. Assign this the default value used in production (e.g., true).
  2. Create the Override: Type a new property path for your target environment (e.g., dev/dialogs/enabled.debug) and click Add.
  3. Assign the Contextual Value: A new sub-property will appear. Set this to your development value (e.g., true).

.debug vs .editor

To master Godot’s environment management, you must understand a fundamental quirk of its architecture: the Godot Editor is a Godot game.

Because of this, the engine strictly separates the editor context from the runtime context. Mixing these up is a common source of tool-scripting bugs.

Feature Tag Context Execution State Use Case
.editor Engine UI Active immediately when opening the project. Plugin scripts, @tool scripts, 2D/3D viewport gizmos. Never used for testing game logic.
.debug Playtest Runtime Active the millisecond you hit the Play (▶️) button. Developer cheats, verbose logging, debug UI overlays.
None (Release) Exported Build The default state for exported .exe, .apk, or .app files. Final production telemetry, production servers, standard gameplay.

If you want a behavior to change only when you test the game, always use .debug.

Reading Variables in GDScript

Creating the override in the editor is only half the battle; your code needs to read it correctly. Godot exposes two different functions for accessing project settings, and choosing the wrong one will completely break your environment pipeline.

❌ ProjectSettings.get_setting(“path”)

  • Behavior: Reads the raw, base value straight from project.godot.
  • The Trap: This function is entirely blind to your environment context. It completely ignores Feature Tags. If your base value is true, it will always return true, even if you have a .debug override set to false.

✅ ProjectSettings.get_setting_with_override(“path”)

  • Behavior: Dynamically evaluates the current runtime environment.
  • The Advantage: It scans the active Feature Tags (e.g., detecting that it is running in a .debug context). If it finds a matching override (like path.debug), it injects that specific value. If no tag is active (Release context), it safely falls back to the base value.

Example

By leveraging overrides, your scripts no longer need to care whether they are in development or production. The architecture handles the logic, leaving your code clean and declarative.

# The script is agnostic to the environment (Dev vs Production).
# The engine automatically delivers the correct bool via the override.

func _ready() -> void:
    if ProjectSettings.get_setting_with_override("dev/dialogs/enabled"):
        show_debug_dialogs()
    else:
        on_dialog_finished()

Stop writing manual debug conditionals. Trust your engine’s architecture, utilize Feature Tags, and keep your production codebase pristine. How is your team currently handling environment pipelines? Let’s discuss in the comments below.

See you in the next digital spell!

Pazalla
Pazalla The Digital Spell Founder