Optimizing GDScript code
2020/08/18
- Type
- Learning Resource
- Format
- Study Guide
- Version
- Godot 3.x
- Subject Tags
- Code
- MIT
- Game Assets
- CC BY-NC-SA
- All else
- 2016-2026, GDQuest© - All rights reserved
- Created
- Updated
- 2020/08/18
- 2020/08/18
Some code optimizations are universal across most programming languages. Some are specific to GDScript.
In this guide, we share some general tips and GDScript-specific techniques to improve your code's execution speed in Godot 3.2.
We recommend you only optimize code you know is slowing down the game or hurting your players' experience. You should always use the profiler and measure how specific functions impact performance.
Things such as improvements to the GDScript compiler may remove the need for some of these optimizations.
To learn to use it and measure your code's performance, read our Godot profiler guide.
Suppose you have an expression that always returns the same value. In that case, you should execute it ahead of time and store the result in a variable. A prime example in Godot is get_node() or its shorthand $. There is no need to call get_node() more than once per node. It has a cost, and repeating it throughout a single frame is wasteful.
Avoid:
var direction
func _process(delta):
var turret = $Turret
direction = Vector2.UP.rotated(turret.rotation)Prefer:
var direction
onready var turret = $Turret
func _process(delta):
direction = Vector2.UP.rotated(turret.rotation)Then, there is also no need to do more math than you need to. Cache the results of values that will stay constant throughout a loop before entering the loop.
# Slow
var alloy_strength = 1.0
var alloy_thickness = 5.0
var alloy_layers = 15.0
for robot in army:
robot.armor = pow(alloy_strength * alloy_thickness, alloy_layers) + robot.native_armor
# Faster
var alloy_strength = 1.0
var alloy_thickness = 5.0
var alloy_layers = 15.0
# Here, we're calculating the base armor rating of robots outside of the loop
var base_armor = pow(alloy_strength * alloy_thickness, alloy_layers)
for robot in army:
robot.armor = base_armor + robot.native_armorIn small arrays that are not accessed often, iterating over nested arrays over a single array is negligible. But for large sets of data or arrays you access frequently, the time difference adds up.
# 0.115443 seconds
for x in 1000:
for y in 1000:
var element = my_array[y][x]
# 0.108107 seconds
for x in 1000:
for y in 1000:
var element = my_array[y * 1000 + x]
# 0.062938 seconds, about 45% faster than the first example
for i in 1000000:
var element = my_array[i]The most interesting part comes when using Godot's native iterator instead of using indices to access an array's elements. Both 1D and 2D arrays get similar, superior speeds with code like this:
# 0.048952 seconds, almost 60% faster than the first example
for x in 1000:
for element in my_array[x]:
#...
# 0.047986 seconds
for element in my_array:
#...Of course, you do not always have the luxury of accessing elements in any order. Sometimes, you need to know the position of the element in the array for more processing. But it's a tradeoff to keep in mind.
If you want high performing arrays in GDScript, the main takeaways are:
for element in array is faster than using indices, e.g. for i in array.size().Whenever you add or remove an element at a given position within an array, Godot has to resize the array and move all its elements. The cost of this operation is proportional to the number of entries in the array. When the removed element is the last one, the engine only needs to resize the array, discarding the last value.
Favor pop_back() and push_back() or append() when removing or adding elements to an array. Avoid pop_front() and push_front().
Choosing the right data structure is not a GDScript-specific issue, but the data structures you use affect all algorithms.
Here are three rules of thumb for choosing data structures with GDScript:
For more information, read the Data preferences page in the official manual.
Calling the print() function is expensive. You don't want to export your games with many print statements left in.
Instead, you can use the built-in debugger to inspect your game at runtime. Another option is to call print_debug(), which only runs in your game's debug builds or when testing it from the editor.
print() involves passing data to your computer's peripherals, like the display, which is slow. The print buffer also has a finite amount of space available, and it easy to fill it up. When the buffer is full, it pauses while outputting its content, which slows Godot down even further and prompts it to give the error [output overflow, print less text!].It can have a significant impact on the framerate at no benefit to the end-user.
If you want to read a large dump of text or data, which is useful to analyze errors that are hard to track down, output text to a log file. You can do so by creating an Autoload dedicated to logging:
extends Node
onready var log = File.new()
func _ready():
log.open("res://log.txt", File.WRITE)
func _exit_tree():
log.close()
func print_msg(message, source):
var current_time = OS.get_time()
log.write_line("%s (%s:%s:%s): %s" % [source, current_time.hour, current_time.minute, current_time.second, message])Every time you call a function, the compiler creates an object to store some information about it. One piece of information is which function called the current one, allowing the compiler to return to the caller. Recursion, that is to say, having a function call itself, causes these objects to stack up.
Recursion can be powerful and lead to code that's easy to read. For small recursion loops, the performance impact is negligible. You shouldn't optimize them away just because you are using recursion. But if you are looking to get more optimal code, consider converting recursive functions to a loop to avoid excessive calls.
# Loop over nodes recursively to find one of a given type.
# This is a recursive function as it calls itself.
func find_by_type_name_recursive(parent, type_name):
for child in parent.get_children():
if child.get_class() == type_name:
return child
else:
var result = find_by_type_name_recursive(child, type_name)
if result:
return result
return null
# Same as the previous function but using a while loop.
# The deeper the scene tree, the higher the performance gain.
func find_by_type_name(parent, type_name):
var parent_stack = [parent]
while parent_stack.size() > 0:
var current = parent_stack.pop_back()
if current.get_class() == type_name:
return current
for child in current.get_children():
parent_stack.push_back(child)
return nullFunction calls are slower than instructions as the compiler needs to store some state for them. Instead of calling a function many times in a loop, putting the loop inside a single function is significantly faster.
var tiles = []
for i in range(1000):
# We're calling `create_random_tile` one thousand times
var new_tile = create_random_tile()
tiles.append(new_tile)
func create_random_tile():
#...Is going to be much slower than:
# Here, we only have one function call
var tiles = create_random_tiles()
func create_random_tiles():
var tiles = []
for i in range(1000):
# ...
return tilesYou can use the match keyword as an equivalent of chains of if, elif, else statements. They can look a bit like case statements in some languages. Currently though, match is a little slower than if for equivalent code. In my tests, the speed difference was about 15% to 20%.
match student.eye_color:
"Green":
#...
"Blue":
#...
"Brown":
#...
"Black":
#...
_:
#...
# About 20% faster than the code above.
if student.eye_color == "Green":
#...
elif student.eye_color == "Blue":
#...
elif student.eye_color == "Brown":
#...
elif student.eye_color == "Black":
#...
else:
#...As always, the difference in execution speed is negligible in code that only runs once. It only starts to add up in loops.
Use the return keyword in functions or continue and break in loops to skip instructions you don't need to run.
As soon as you have found the value you were looking for, try to leave it as soon as possible.
func get_inventory_from(container):
if not container.inventory:
return null
var inventory_array = []
for block in container.inventory:
inventory_array.append(block.get_slots())
if inventory_array.size() == 0:
return null
return parse_inventory(inventory_array)You have some optimizations that cause large differences, like removing the elements from the back of an array or caching expensive results. Then, you have the little things you probably shouldn't sweat but can be good practices. Gains here are much smaller than with previous optimization tips.
Here are three examples:
or or and boolean operation, try to put conditions that are more likely to return true on the left side of or, and conditions that are more likely to return false on the left side of and.match statements, try to put the candidates that appear more commonly near the chain's top.distance_squared_to instead of distance_to to avoid a square root operation. If you divide by a value more than three times, pre-calculate that and store it in a variable.Even with all the optimization you try to add, GDScript might fail you with performance-intensive algorithms, like heavy procedural generation or heatmap pathfinding.
If this happens, you will want to look into using either C# or C++ GDNative libraries.
Don't stop here. Step-by-step tutorials are fun but they only take you so far.
Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.
Get help from peers and pros on GDQuest's Discord server!
20,000 membersJoin ServerThere are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!
Sponsor this library by learning gamedev with us onGDSchool
Learn MoreImprove and build on assets or suggest edits onGithub
Contributeshare this page and talk about GDQUest onRedditYoutubeTwitter…
Site in BETA!found a bug?