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

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.
gdscriptenum 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:
gdscriptprint(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.
gdscriptenum 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:
gdscriptclass 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.
gdscriptenum 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
gdscriptcreature_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.
gdscriptenum 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
gdscriptvar 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.
gdscriptenum 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.
gdscriptenum 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.
gdscriptfunc 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:
gdscriptfunc 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.
gdscriptenum 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
gdscriptfunc 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.
gdscriptvar 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:
gdscriptfunc 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
vs32
).
Reverse Lookup Cache
gdscriptvar 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.
gdscriptenum 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
Pitfall | Symptom | Fix |
---|---|---|
Reordering enum without explicit values | Save / network mismatch | Assign fixed numeric values |
Using strings instead of enum | Typos cause silent bugs | Replace with enum constants |
Large match duplicated across files | Fragile edits | Centralize logic or table-drive |
Flags not power of two | Overlapping bits | Always use 1 << n pattern |
Forgetting fallback in match | Crashes on unexpected | Add _: 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:
gdscriptfunc 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:
gdscriptfor 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:
gdscriptvar 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:
gdscriptfunc 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:
gdscriptenum 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:
gdscriptconst 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:
gdscriptfunc 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:
gdscriptvar 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:
- Reserve gaps (e.g., assign 10,20,30) for additions.
- Maintain a legacy->current map for migration.
- Use a base enum + separate data-driven categories (dictionary keyed by string for modded extras).
- 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!!!