godot

Godot Enum - A Comprehensive Guide (GDScript 4.x)

Cover Image for Godot Enum - A Comprehensive Guide (GDScript 4.x)
11 min read
#godot

Enums in Godot (GDScript) give names to numeric constants, making code more expressive, maintainable, and less error-prone than using raw numbers or strings for states, modes, item rarities, AI phases, network op codes, etc. While simple on the surface—enum Direction { UP, DOWN }—there are deeper patterns: namespacing, value assignment, bitflags, combining with Dictionaries, UI binding, pattern matching, strongly typed function parameters (Godot 4), and serialization across save or network boundaries.

This 2025 deep guide covers: creating enums, scoping strategies, custom values, bitmask enums, using enums with state machines and animation logic, mapping enums to data, pattern matching workflows, editor tools, interoperability with JSON, performance nuances, and common pitfalls.

1. What Is an Enum in GDScript?

An enum is a syntactic convenience for generating a set of named integer constants. Each name maps to an integer index (or custom value) starting from zero unless overridden.

gdscript
enum Direction { UP, RIGHT, DOWN, LEFT } print(Direction.UP) # 0 print(Direction.LEFT) # 3

Using symbolic names improves readability (e.g., Direction.LEFT vs 2) and reduces magic numbers scattered across code.

2. Declaring Enums (Core Forms)

Below are the three main declaration styles: inline simple list, explicit mapping, and anonymous array style that returns a Dictionary.

gdscript
# 1. Sequential auto values enum WeaponType { SWORD, BOW, STAFF } # 2. Explicit numeric values (gaps allowed) enum DamageType { PHYSICAL = 10, FIRE = 20, ICE = 30, POISON = 90 } # 3. Using array-like syntax (older style still works) var ItemQuality = enum { COMMON, RARE, EPIC }

Use explicit numeric values when integrating with external systems (network protocol codes, save file schemas) so reordering the enum list doesn’t silently change meaning.

Inspecting Enum Contents

The enum generates a Dictionary mapping names to ints:

gdscript
print(DamageType) # {"PHYSICAL":10, "FIRE":20, "ICE":30, "POISON":90}

You can iterate over keys/values like any normal dictionary when building UI lists.

3. Custom Values & Stability

If you rely on numeric stability across builds (e.g., multiplayer or mod data), ALWAYS assign explicit values and never repurpose a numeric slot for a different semantic meaning.

gdscript
enum NetOp { PING = 1, PONG = 2, AUTH = 5, MOVE = 8 }

Reserving gaps (1,2,5,8) lets you insert future operations without renumbering existing codes—preserving backward compatibility with stored packets or replays.

4. Namespacing & Scope Strategies

Enums declared at the top of a script become that script’s members. For shared enumerations:

gdscript
# file: game_enums.gd class_name GameEnums enum SceneState { LOADING, MENU, IN_GAME, PAUSED } enum Rarity { COMMON, UNCOMMON, RARE, LEGENDARY }

Later reference them via GameEnums.Rarity.RARE. This centralizes constants, reducing duplication.

Local Scoped Enums

You can place enums inside classes to limit their public surface:

gdscript
class EnemyAI: enum Phase { IDLE, PATROL, CHASE, ATTACK }

Use scoping to avoid collisions when different systems need overlapping symbolic names (e.g., multiple State enums).

5. Bitflag Style Enums

Sometimes you need combinations (e.g., collision layers, ability flags). Although Godot has dedicated layer masks, you can model your own using bit-shifted values.

gdscript
enum AbilityFlags { NONE = 0, CAN_FLY = 1 << 0, CAN_SWIM = 1 << 1, CAN_BURROW = 1 << 2, CAN_TELEPORT = 1 << 3 } var creature_flags = AbilityFlags.CAN_FLY | AbilityFlags.CAN_SWIM func has_flag(all, flag): return (all & flag) != 0 print(has_flag(creature_flags, AbilityFlags.CAN_SWIM)) # true

Choose power-of-two constants so OR’ing them accumulates independent capabilities. Prefer this pattern when many boolean toggles would otherwise clutter a dictionary or script.

Adding / Removing Flags

gdscript
creature_flags |= AbilityFlags.CAN_BURROW # add creature_flags &= ~AbilityFlags.CAN_FLY # remove

Using flags shrinks memory use over many objects compared to multiple separate booleans, and can be faster for aggregate queries.

6. Enums in State Machines

Using an enum for state IDs clarifies transitions and enables validation.

gdscript
enum PlayerState { IDLE, RUN, JUMP, DASH } var state: PlayerState = PlayerState.IDLE func set_state(next: PlayerState): if state == next: return _exit_state(state) state = next _enter_state(state)

Typed variable annotation (var state: PlayerState) improves editor hints and catches accidental assignment of unrelated integers.

Transition Table Pattern

gdscript
var transitions := { PlayerState.IDLE: [PlayerState.RUN, PlayerState.JUMP], PlayerState.RUN: [PlayerState.IDLE, PlayerState.JUMP, PlayerState.DASH], PlayerState.JUMP: [PlayerState.RUN], PlayerState.DASH: [PlayerState.RUN] } func can_go(to: PlayerState) -> bool: return to in transitions.get(state, [])

A dictionary keyed by enum values makes legality checks O(1) and human-readable.

7. Mapping Enums to Data

Often enums index into associated data tables. This avoids giant match blocks when you simply need properties.

gdscript
enum Element { FIRE, WATER, EARTH, AIR } var element_data := { Element.FIRE: {"color": Color.RED, "weak": Element.WATER}, Element.WATER: {"color": Color.CYAN, "weak": Element.EARTH}, Element.EARTH: {"color": Color.BROWN, "weak": Element.AIR}, Element.AIR: {"color": Color.WHITE, "weak": Element.FIRE}, } func get_weakness(e: Element) -> Element: return element_data[e]["weak"]

You can later extend entries (e.g., add sound, particle) without modifying logic.

Enum to Array Index Pattern

If enum values are continuous starting at 0, store parallel arrays for performance.

gdscript
enum DamageTier { T0, T1, T2, T3 } var damage_values := [5, 9, 15, 25] func base_damage(t: DamageTier) -> int: return damage_values[int(t)]

This avoids dictionary hash overhead in hot loops (e.g., bullet calculations).

8. Pattern Matching with Enums

match provides exhaustive branching clarity. Provide a fallback for robustness.

gdscript
func describe_direction(d: Direction) -> String: match d: Direction.UP: return "Moving Up" Direction.RIGHT: return "Moving Right" Direction.DOWN: return "Moving Down" Direction.LEFT: return "Moving Left" _: return "Unknown"

If you add a new enum member but forget to handle it, tests or logging can reveal the fallback path—keeping code safer than raw number comparisons.

Combining Match with Flags

Flags aren’t amenable to basic match; handle them via bit tests:

gdscript
func describe_flags(flags: int): var parts := [] if has_flag(flags, AbilityFlags.CAN_FLY): parts.append("Fly") if has_flag(flags, AbilityFlags.CAN_SWIM): parts.append("Swim") if has_flag(flags, AbilityFlags.CAN_BURROW): parts.append("Burrow") if parts.is_empty(): return "None" return ", ".join(parts)

9. Enums & UI Binding

Enums frequently feed dropdowns / option buttons.

gdscript
enum Difficulty { EASY, NORMAL, HARD, NIGHTMARE } var difficulty_labels := { Difficulty.EASY: "Easy", Difficulty.NORMAL: "Normal", Difficulty.HARD: "Hard", Difficulty.NIGHTMARE: "Nightmare" } func populate(menu: OptionButton): for k in difficulty_labels.keys(): menu.add_item(difficulty_labels[k], k)

Passing the enum value as the item’s ID means selection events can map directly back to typed logic.

Persisting Selection

gdscript
func save_settings(current: Difficulty) -> Dictionary: return {"difficulty": int(current)} func load_settings(d: Dictionary) -> Difficulty: return Difficulty(d.get("difficulty", Difficulty.NORMAL))

Coercing to int ensures small stable footprint in save files.

10. Serialization & Networking

Because enums are integers, serialization is straightforward. Avoid saving raw enum dictionaries (names) unless human readability outweighs size.

gdscript
var payload := {"op": NetOp.MOVE, "x": 12, "y": 6} var json = JSON.stringify(payload) # compact numeric op

If you need human-readable logs, store both name + value:

gdscript
func enum_with_label(e_dict: Dictionary, value: int) -> Dictionary: var name = e_dict.keys().filter(func(k): return e_dict[k] == value)[0] return {"name": name, "value": value}

Cache a reverse lookup map for speed if you perform this often.

11. Performance Notes

  • Accessing enum constants is O(1) (they’re pre-resolved integers).
  • Using dictionaries keyed by enums is faster than using strings for frequent lookups.
  • Avoid linear searching for enum names repeatedly—build reverse maps once.
  • For bitflags, prefer shift constants over manual integer literals for readability (1 << 5 vs 32).

Reverse Lookup Cache

gdscript
var reverse_damage := {} func _ready(): for k in DamageType: reverse_damage[DamageType[k]] = k

Now reverse_damage[20] returns "FIRE" instantly.

12. Advanced Pattern: Enum-Driven Components

Use an enum to tag which optional systems to initialize.

gdscript
enum Feature { AUDIO, PHYSICS, AI } var feature_init := { Feature.AUDIO: func(): _init_audio(), Feature.PHYSICS: func(): _init_physics(), Feature.AI: func(): _init_ai() } func init_features(active: Array[Feature]): for f in active: feature_init[f].call()

This maps symbolic constants to behavior without giant switch statements. Clean, extensible, testable.

13. Common Pitfalls & Fixes

PitfallSymptomFix
Reordering enum without explicit valuesSave / network mismatchAssign fixed numeric values
Using strings instead of enumTypos cause silent bugsReplace with enum constants
Large match duplicated across filesFragile editsCentralize logic or table-drive
Flags not power of twoOverlapping bitsAlways use 1 << n pattern
Forgetting fallback in matchCrashes on unexpectedAdd _: default path

14. Frequently Asked Questions

1. Can enums be typed in function parameters?

Yes—annotate: func move(dir: Direction):. Although under the hood the value is still an int, this communicates intent, activates better autocomplete, and surfaces warnings in some tooling. Example:

gdscript
func move(dir: Direction): match dir: Direction.UP: velocity = Vector2(0, -speed) Direction.DOWN: velocity = Vector2(0, speed) Direction.LEFT: velocity = Vector2(-speed, 0) Direction.RIGHT: velocity = Vector2(speed, 0) _: push_warning("Unhandled direction %s" % dir)

If you fear rogue integers, assert: assert(dir in Direction.values()) at dev time.

2. Are enum values mutable at runtime?

No. Once parsed, the mapping name->int is frozen. You cannot append new members or change assigned values. For extensibility (mods, DLC):

  • Use a data table (Dictionary or Resource) that maps strings to ids you allocate dynamically.
  • Reserve numeric gaps in explicit enums (10,20,30) for forward additions.
  • Maintain a versioned translation map for older save files. Trying to “patch” an enum by editing order after shipping risks desync with saved or networked data.

3. Can I iterate enum members?

Yes—treat it like a dictionary of names to ints. Patterns:

gdscript
for name in Direction: # name is String var value = Direction[name] print(name, value)

Get numeric values only: var vals = Direction.values(). For UI labels capitalized:

gdscript
var labels := [] for n in Direction.keys(): labels.append(n.capitalize())

Do not rely on iteration order for gameplay sequencing—explicitly define an ordered array if needed.

4. How to convert int back to enum safely?

Write a helper that validates membership and supplies a fallback:

gdscript
func to_direction(v: int, fallback := Direction.UP) -> int: if v in Direction.values(): return v push_warning("Invalid direction id %s; using fallback" % v) return fallback

For high-frequency conversions build a Set for O(1) membership (dictionary with dummy true values).

5. Should I localize enum labels directly?

Avoid embedding human text into enum names. Keep names stable, map them to translation keys:

gdscript
enum Difficulty { EASY, NORMAL, HARD } var difficulty_tr := { Difficulty.EASY: "difficulty.easy", Difficulty.NORMAL: "difficulty.normal", Difficulty.HARD: "difficulty.hard" } func difficulty_label(d: Difficulty) -> String: return tr(difficulty_tr[d])

This isolates localization concerns and allows renaming UI strings without touching logic.

6. Difference between enum and constants block?

Enums: auto-increment (unless explicit), produce a dictionary for introspection, and supply convenience arrays via .keys() / .values(). Constants block:

gdscript
const DIR_UP = 0 const DIR_RIGHT = 1

Downsides of constants: no built-in reverse lookup, easy to create collisions, harder to iterate generically. Use constants when identifiers are not integers (e.g., strings, packed structs) or when you need mixed-type constants in one bloc.

7. Can I serialize names instead of numbers?

Yes. Trade-offs:

  • Numbers: compact, faster parse, stable if you never change assignments.
  • Names: readable diffs, easier manual editing, resilient if numeric slots shift but names stay. Hybrid approach:
gdscript
func serialize_state(s: PlayerState) -> Dictionary: return {"id": int(s), "name": _player_state_name(s)} func _player_state_name(s): for k in PlayerState: if PlayerState[k] == s: return k return "UNKNOWN"

On load prefer id; if missing/unmapped fallback to name resolution.

8. Are bitflags always faster than booleans?

No. They help when:

  • Many independent binary capabilities per object (memory density).
  • Frequent composite checks (if flags & (A|B)), batching, or serialization packing. They hurt clarity when only a handful of toggles exist or designers inspect raw data. Start with booleans; refactor to flags only after profiling.

9. How to list raw numeric values quickly?

Use Direction.values() for integers, Direction.keys() for names. Building a reverse map for debug:

gdscript
var dir_reverse := {} for name in Direction: dir_reverse[Direction[name]] = name

Need sorted numeric list: var sorted = Direction.values(); sorted.sort()—useful for validation tests.

10. Can I extend an enum later dynamically?

No dynamic extension. Strategies to future-proof:

  1. Reserve gaps (e.g., assign 10,20,30) for additions.
  2. Maintain a legacy->current map for migration.
  3. Use a base enum + separate data-driven categories (dictionary keyed by string for modded extras).
  4. Combine a fixed enum with auxiliary string subtype fields. Choose the least complex option that satisfies anticipated growth.

Conclusion

Enums in Godot GDScript offer far more than a replacement for magic numbers—they structure game states, streamline UI, drive data tables, unify network op codes, compress feature toggles into flags, and clarify intent across a codebase. By embracing explicit values for stability, table-driven mappings for extensibility, and bitflags for capability composition, you turn simple constants into robust architectural anchors.

Adopt these patterns incrementally: start by replacing loose integers, then consolidate scattered switch logic into enum-indexed dictionaries, finally introduce stable numeric assignments for any data crossing save/network boundaries. Your future maintenance burden drops—and your code reads like a design document.

Follow and Support me on Medium and Patreon. Clap and Comment on Medium Posts if you find this helpful for you. Thanks for reading it!!!

Related Blogs

View All

Other Blogs

View All