This guide covers best practices when writing GDScript code. We use it to keep our code clean and maintainable. It’s meant as a complementary resource to the oficial GDScript guidelines.
The ideas in this article are inspired by best practices from a variety of languages. We draw from the work of the Python community, some functional programming principles, and the official GDScript documentation:
Godot is mostly object-oriented and offers its own tools such as signals and the node tree to make objects communicate. As a result, it’s not always easy to apply principles and techniques from other programming languages to it.
We wrote these GDScript programming guidelines with a few goals in mind:
We follow the same code order from the official GDScript style guide. Here is a complete example that follows these guidelines:
# Hierarchical State machine for the player.
# Initializes states and delegates engine callbacks (_physics_process, _unhandled_input) to the state.
class_name StateMachine
extends Node
signal state_changed(previous, new)
export var initial_state := NodePath()
onready var state: State = get_node(initial_state) setget set_state
onready var _state_name := state.name
func _init() -> void:
add_to_group("state_machine")
func _ready() -> void:
connect("state_changed", self, "_on_state_changed")
state.enter()
func _unhandled_input(event: InputEvent) -> void:
state.unhandled_input(event)
func _physics_process(delta: float) -> void:
state.physics_process(delta)
# Replaces the current state with the state at `target_state_path` if the path
# is valid. Passes the `msg` dictionary to the new state's `enter` function.
func transition_to(target_state_path: String, msg: Dictionary = {}) -> void:
if not has_node(target_state_path):
return
var target_state := get_node(target_state_path)
assert(target_state.is_composite == false)
state.exit()
self.state = target_state
state.enter(msg)
Events.emit_signal("player_state_changed", state.name)
func set_state(value: State) -> void:
state = value
_state_name = state.name
func _on_state_changed(previous: Node, new: Node) -> void:
print("state changed")
emit_signal("state_changed")
Start with the optional class_name
if needed. Then add the extends
keyword if the class extends a built-in type. Following that, you should have the class’s docstring:
# A brief description of the class's role and functionality.
#
# A longer description if needed, possibly of multiple paragraphs. Properties and method names
# should be in backticks like so: `_process`, `x` etc.
#
# You can use *markdown* in the docstrings.
#
# Keep lines under 100 characters long.
class_name MyNode
extends Node
Signals go first and don’t use parentheses unless they pass function parameters. Use the past tense to name signals. Append _started
or _finished
if the signal corresponds to the beginning or the end of an action.
signal moved
signal talk_started(parameter_name)
signal talk_finished
From Godot 3.2, you can write docstrings above any property, signal, or function as a series of comments above their definition, and the GDScript language server will show them in the docs completion. You can also use that to create a code reference with our tool GDScript Docs Maker.
After that enums, constants, exported, public (regular name), and pseudo-private (starting with _
) variables, in this order.
Enum type names should be in CamelCase
while the enum values themselves should be in ALL_CAPS_SNAKE_CASE
. This order is important because exported variables might depend on previously defined enums and constants.
enum TileTypes { EMPTY=-1, WALL, DOOR }
const MAX_TRIALS := 3
const TARGET_POSITION := Vector2(2, 56)
export var number := 0
export var is_active := true
Note: for booleans, always include a name prefix like is_
, can_
, or has_
.
After this are public and pseudo-private member variables. Their names should use snake_case
and _snake_case_with_leading_underscore
respectively.
Define setters and getters when properties alter the object’s state or if changing the property triggers methods. Care needs to be taken when doing this because we can easily loose track of hidden alterations and behaviors.
Include a docstring in the setters/getters if they modify the node/class state in complex ways.
Start with a leading underscore when writing setters or getters for private variables just like the variable.
var animation_length := 1.5
var tile_size := 40
var side_length := 5 setget set_side_length, get_side_length
var _count := 0 setget _set_count, _get_count
var _state := Idle.new()
Place onready
variables right before _init
and/or _ready
functions to visually connect these with their usage inside the _ready
function.
onready var timer := $HungerCheckTimer
onready var ysort := $YSort
Next define virtual methods from Godot (those starting with a leading _
, e.g. _ready
). Always leave 2 blanks lines between methods to visually distinguish them from other code blocks.
func _init() -> void:
pass
func _process(delta: float) -> void:
pass
For signal callbacks, we use Godot’s convention, _on_NodeName_signal_name
:
func _on_Quest_started(which: Quest) -> void:
...
You should remove NodeName
if the object connects to itself:
class_name HitBox
extends Area2D
func _ready() -> void:
connect("area_entered", self, "_on_area_entered")
Then define public methods. Include type hints for variables and the return type. You can use a brief comment to describe what the function does and what it returns.
Start the sentence with Returns
when describing the return value. Use the present tense and direct voice. See Godot’s documentation writing guidelines for more information.
func can_move(cell_coordinates: Vector2) -> bool:
return grid[cell_coordinates] != TileTypes.WALL
Use return
only at the beginning and end of functions. At the beginning of the function we use return
as a guard mechanism if the conditions for executing the function aren’t met.
Don’t return in the middle of the method. It makes it harder to track returned values.
Here’s an example of a clean and readable method:
func _set_elements(elements: int) -> bool:
# Sets up the shadow scale, number of visual elements and instantiates as needed.
# Returns true if the operation succeeds, else false
if (not has_node("SkinViewport") or
elements > ELEMENTS_MAX or
not has_node("Shadow")):
return false
# If the check succeeds, proceed with the changes
var skin_viewport := $SkinViewport
var skin_viewport_staticbody := $SkinViewport/StaticBody2D
for node in skin_viewport.get_children():
if node != skin_viewport_staticbbody:
node.queue_free()
var interval := INTERVAL
var r := RandomNumberGenerator.new()
r.randomize()
for i in range(elements):
var e := Element.new()
e.node_a = "../StaticBody2D"
e.position = skin_viewport_staticbody.position
e.position.x += r.randf_range(interval.x, interval.y)
interval = interval.rotated(PI/2)
skin_viewport.add_child(e)
$Shadow.scale = SHADOW.scale * (1.0 + elements/6.0)
return true
We use static types to help write more insightful code and help avoid errors.
At the time of writing, static typing doesn’t provide any performance boosts in GDScript. But it gives you better code completion and error reporting. In the future, it should give you major performance improvements over dynamic code as well.
To get started with GDScript’s type hints, read Static typing in GDScript in the official manual.
Normally, you define typed variables like this:
var direction: Vector2 = get_move_direction()
But if get_move_direction
has a return type annotation, Godot can infer the type of the variable for us. In that case, we only need to add a colon after the variable’s name:
func get_move_direction() -> Vector2:
var direction := Vector2(
...
)
return direction
var direction := get_move_direction() # The variable's type is Vector2
Let Godot infer the type whenever you can. It’s less error prone because the system keeps better track of types than we humanly can. It also pushes us to have proper return values for all the functions and methods that we write.
Since the compiler doesn’t enforce static types, sometimes we have to help it out. Take the following example:
var array := [1, "Some text"]
var text: String = array.pop_back()
The Array
can contain values with different types. In the example above, we have both an int
and a String
stored in the array. If you only wrote var text := array.pop_back()
, Godot would complain because it doesn’t know what type the pop_back
method should return.
If we open the code reference with Shift+F1 and search for the method, we see that:
Variant pop_back()
Remove the last element of the array.
Variant
is a generic type that can hold any type Godot supports. That’s why we have to explicitly write variable types when dealing with these functions: var text: String = arr.pop_back()
.
You’ll get this issue with all built-in methods that return the engine’s Variant
type.
null
referencesUse null
only when you have to. Instead, think about alternatives to implement the same functionality with other types.
Using null
is often a lost opportunity to have a value that makes sense instead, like Vector2.ZERO
, or to simplify code, removing unnecessary state checks, i.e.:
if not variable:
return
With type hints and type inference you will naturally avoid null
though:
var speed := Vector2.ZERO
var path := TrajectorySpline.new()
It’s impossible to get rid of null
completely because GDScript relies on built-in functions that work with null
values.
None
, null
, NULL
, and similar references could be the biggest mistake in the history of computing. Here’s a detailed explanation from the man who invented it himself: Null References: The Billion Dollar Mistake.
If you need comments to explain most of what your code does, you can most likely rewrite it to make it more transparent for everyone. When working together for an extended period, code readability is essential for everyone to stay productive.
Use clear variable names in plain English and write full words. E.g. character_position
and not char_pos
. The same goes for method names.
Do not repeat words in the method’s name and its arguments. E.g. write Inventory.add(item)
, not Inventory.add_item(item)
. The same goes for signals.
Use plain verbs instead of repeating the class’s name in signals:
class_name Event
extends Node
signal started
signal completed
You may use short variable names inside of your methods, for local variables, to avoid repeating a type hint for instance.
In the example below, the variable e
, an instance of Element
, only appears in 4 consecutive lines so the code stays readable:
func _set_elements(elements: int) -> bool:
...
for i in range(elements):
var e := Element.new()
e.node_a = "../StaticBody2D"
e.position = skin_viewport_staticbody.position
...
Your code should be self-explanatory whenever possible. Sometimes you may have a long block of code that you can’t change, or have some strange code to work around an engine bug.
In these cases, writing a short comment above the corresponding block can save everyone a lot of time including your future self.
In this example, the code involves transforming and multiplying matrices to calculate a position in Godot’s 2D viewport. A one-line comment can capture what it does and avoid having to make sense of the calculations:
func drag_to(event_position: Vector2) -> void:
# Calculate the position of the mouse cursor relative to the RectExtents' center
var viewport_transform_inverse:= rect_extents.get_viewport().get_global_canvas_transform().affine_inverse()
var viewport_position: Vector2 = viewport_transform_inv.xform(event_position)
var transform_inverse:= rect_extents.get_global_transform().affine_inverse()
var target_position: Vector2 = transform_inv.xform(viewport_position.round())
Here’s a comment that explains why a seemingly strange line of code is necessary so another developer doesn’t remove it accidentally:
extends BattlerAI
func choose_action(actor: Battler, targets: Array = []):
# We use yield even though the action is instantaneous
# because the combat system expects this method to use a coroutine
yield(get_tree(), "idle_frame")
...
We follow some guidelines to keep the name of our files meaningful and consistent.
The top-level folders include:
assets
, shared files like images, sounds, music, text produced outside Godot.src
, the source code of the game. It includes all scenes and GDScript files which are all part of the source.Never include spaces in the filenames. Spaces can cause issues with command line tools, which we use to automate tasks.
We generally name files, variables, and classes starting with keywords that help group and distinguish similar elements.
For example, instead of:
var walk_speed
var run_speed
var sprint_speed
var max_speed
We favor:
var speed_walk
var speed_run
var speed_sprint
var speed_max
This helps to group and find related variables through autocompletion. In this case, if you type sp
, all four speed-related variables will appear in the completion menu.
Use PascalCase
for folder names in the src
folder as they represent game systems or groups of systems.
Scenes and script files also use PascalCase
as they represent classes. This is also how Godot names them by default:
Quests/
Quest.gd
Journal.gd
QuestDirector.gd
Use lowercase for the folder names to distinguish them from the source code.
Name the assets using snake_case
:
body.png
arm_right.png
sword_hit.ogg
theme.tres