Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Game Development with Godot & GDScript  Module 2 – GDScript fundamentals through game data

Functions, scope and variadic arguments

Write GDScript functions with typed parameters, defaults and return types, keep local and member names apart, and use static, recursive and variadic functions.

  • Beginner
  • 15 minutes
  • Examples run with godot 4.7.2-stable and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Declare functions with typed parameters, default values and return types
  • Explain local, member and global scope, and avoid shadowing a name
  • Write static and recursive functions, and replace deep recursion with a loop
  • Use variadic parameters (Godot 4.5 and later) and callv() to pass any number of arguments

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare functions with typed parameters, default values and return types
  • Explain local, member and global scope, and avoid shadowing a name
  • Write static and recursive functions, and replace deep recursion with a loop
  • Use variadic parameters (Godot 4.5 and later) and callv() to pass any number of arguments

Before you start

On this page

A recursive function with no way to stop called itself 2,047 levels deep on Godot 4.7.2 before the engine halted it with “Stack overflow”. Functions are how a game’s rules get names, and the rules for writing them, typed parameters, return values, scope and recursion, decide whether a mistake is caught before the game runs or after a player finds it.

Below you write functions with types and default values and read the messages Godot gives for the usual slips, see where GDScript finds a name and what happens when two share one, write static and recursive functions, and take any number of arguments with the rest parameter that arrived in Godot 4.5.

A function takes typed parameters and returns one value

func declares a function: its name, its parameters in brackets, and after -> the type of the value it returns. Every parameter is required unless it has a default value, and parameters with defaults must come last. Arguments are matched by position, and return ends the call and hands back its value; a function typed -> void returns none.

loot_roll.gd: typed parameters, defaults and a return type GD · loot_roll.gd
extends SceneTree

# Functions with typed parameters, default values and a return type.
func roll_loot(level: int, luck: float = 1.0, guaranteed: bool = false) -> int:
	var gold := level * 10
	if guaranteed:
		return gold * 2
	return roundi(gold * luck)


func announce(item: String) -> void:
	if item.is_empty():
		return
	print("You found ", item, ".")


func _initialize() -> void:
	print("roll_loot(3) = ", roll_loot(3))
	print("roll_loot(3, 1.5) = ", roll_loot(3, 1.5))
	print("roll_loot(3, 1.5, true) = ", roll_loot(3, 1.5, true))
	announce("a silver key")
	announce("")
	quit()
missing_return.gd: a path that returns nothing GD · mistakes/missing_return.gd
extends SceneTree

# A function that promises a String, with a path that returns nothing: a negative score.
func grade(score: int) -> String:
	if score >= 50:
		return "pass"
	elif score >= 0:
		return "fail"


func _initialize() -> void:
	print(grade(70))
	quit()
optional_first.gd: a default before a required parameter GD · mistakes/optional_first.gd
extends SceneTree

# A default value before a parameter without one.
func spawn(count: int = 1, kind: String) -> void:
	print(count, " ", kind)


func _initialize() -> void:
	spawn(3, "slime")
	quit()
bad_calls.gd: four calls Godot refuses GD · mistakes/bad_calls.gd
extends SceneTree

func heal(hp: int, amount: int = 10) -> int:
	return hp + amount


func announce(item: String) -> void:
	print("You found ", item, ".")


func _initialize() -> void:
	print(heal())
	print(heal("30"))
	print(heal(30, 5, 1))
	var said = announce("a map")
	quit()
Functions with defaults, and the mistakes Godot refuses, Godot 4.7.2 Python · functions_session.py
from session import files, run

files("project.godot", "loot_roll.gd", "mistakes=.")
run("""
godot --headless --no-header --script loot_roll.gd
!godot --headless --no-header --check-only --script missing_return.gd
!godot --headless --no-header --check-only --script optional_first.gd
!godot --headless --no-header --check-only --script bad_calls.gd
""")

Output

$ godot --headless --no-header --script loot_roll.gd
roll_loot(3) = 30
roll_loot(3, 1.5) = 45
roll_loot(3, 1.5, true) = 60
You found a silver key.
$ godot --headless --no-header --check-only --script missing_return.gd
SCRIPT ERROR: Parse Error: Not all code paths return a value.
          at: GDScript::reload (res://missing_return.gd:4)
ERROR: Failed to load script "res://missing_return.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --no-header --check-only --script optional_first.gd
SCRIPT ERROR: Parse Error: Cannot have mandatory parameters after optional parameters.
          at: GDScript::reload (res://optional_first.gd:4)
ERROR: Failed to load script "res://optional_first.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --no-header --check-only --script bad_calls.gd
SCRIPT ERROR: Parse Error: Too few arguments for "heal()" call. Expected at least 1 but received 0.
          at: GDScript::reload (res://bad_calls.gd:12)
SCRIPT ERROR: Parse Error: Cannot pass a value of type "String" as "int".
          at: GDScript::reload (res://bad_calls.gd:13)
SCRIPT ERROR: Parse Error: Invalid argument for "heal()" function: argument 1 should be "int" but is "String".
          at: GDScript::reload (res://bad_calls.gd:13)
SCRIPT ERROR: Parse Error: Too many arguments for "heal()" call. Expected at most 2 but received 3.
          at: GDScript::reload (res://bad_calls.gd:14)
SCRIPT ERROR: Parse Error: Cannot get return value of call to "announce()" because it returns "void".
          at: GDScript::reload (res://bad_calls.gd:15)
ERROR: Failed to load script "res://bad_calls.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 functions_session.py

roll_loot(3) used both defaults, luck 1.0 and guaranteed false, and gave 30; roll_loot(3, 1.5) replaced the first default, and roll_loot(3, 1.5, true) both. announce("") printed nothing, because its first line returns early from a void function. Every mistake of the session was found before anything ran, which is what the types buy:

Mistake Godot’s message
a path with no return in a typed function Not all code paths return a value.
a default before a required parameter Cannot have mandatory parameters after optional parameters.
too few or too many arguments Too few arguments for "heal()" call. Expected at least 1 but received 0.
an argument of the wrong type Cannot pass a value of type "String" as "int".
using the result of a void function Cannot get return value of call to "announce()" because it returns "void".

grade() returns nothing for a negative score, so Godot refuses the whole function. Give every path a return, here an else with its own value, or end the function with a return that covers the rest.

Names are found locally first, then in the script, then globally

A name in a function can mean three things. A local variable or a parameter exists only during one call. A member, declared with var or const outside any function, belongs to the script and is shared by all its functions. A global name, such as print(), maxi() or a class like Node2D, is available everywhere. The reference gives the order of the search: local, then class member, then global. Step through the names of scope.gd:

Interactive · stage by stage Where GDScript finds a name, in three places Step through the names of take_damage() and shadowed(). For each, the search stops at the first place that has it.
Where GDScript finds a name, in three places, step 5 of 5. Showing Locals of the call (amount, health (90)), Members of the script (health, MAX_HEALTH), Global names (maxi(), print()). In focus: Members of the script.Locals of the callamount, health (90)Members of the scripthealth, MAX_HEALTHGlobal namesmaxi(), print()

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. amount and left. In take_damage(), amount is a parameter and left a local declared with var. Both are found in the first place GDScript looks, and both are gone when the call returns.
  2. health. take_damage() declares no local named health, so the search goes on to the members of the script and finds var health, the one every function shares.
  3. maxi. maxi is neither a local nor a member, so it is found among the global names of @GlobalScope, with print() and roundi().
  4. A local hides the member. shadowed() declares var health itself. The search finds that local first and stops, so the member stays 0 and the local is 90, as the run printed.
  5. self reaches past it. self.health skips the locals and reads the member. Better still, give the local another name, which the editor asks for with a warning.

scope.gd: a member, locals and a global function GD · scope.gd
extends SceneTree

# Where names live: a member of the script, locals of a function, and the globals of @GlobalScope.
var health := 100          # a member: every function of the script sees it
const MAX_HEALTH := 100    # a constant member


func take_damage(amount: int) -> void:
	var left := health - amount       # left is local: it exists only during this call
	health = maxi(left, 0)            # health is the member; maxi() is a global function
	print("took ", amount, ", health is now ", health)


func shadowed(amount: int) -> void:
	var health := MAX_HEALTH - amount # a local with the member's name hides the member
	print("local health ", health, ", member health ", self.health)


func _initialize() -> void:
	take_damage(30)
	take_damage(90)
	shadowed(10)
	print("after both calls, health = ", health)
	quit()
shadow_global.gd: a parameter named like a global function GD · shadow_global.gd
extends SceneTree

# A parameter named like a global function, max(). It works, but the name now means two things in this function.
func spend(gold: int, max: int) -> int:
	print("max is the parameter: ", max, "; max(3, 4) still calls the global function: ", max(3, 4))
	return mini(gold, max)


func _initialize() -> void:
	print(spend(500, 120))
	quit()
Scope, and two kinds of shadowing made errors, Godot 4.7.2 Python · scope_session.py
from session import files, run

# scope.gd and shadow_global.gd run in a project with Godot's default warning levels, then in strict/, whose
# project.godot turns the two shadowing warnings into errors (level 2).
files("project.godot", "scope.gd", "shadow_global.gd", "strict/project.godot", "scope.gd=strict/scope.gd",
      "shadow_global.gd=strict/shadow_global.gd")
run("""
godot --headless --no-header --script scope.gd
godot --headless --no-header --script shadow_global.gd
cd strict
!godot --headless --no-header --check-only --script scope.gd
!godot --headless --no-header --check-only --script shadow_global.gd
""")

Output

$ godot --headless --no-header --script scope.gd
took 30, health is now 70
took 90, health is now 0
local health 90, member health 0
after both calls, health = 0
$ godot --headless --no-header --script shadow_global.gd
max is the parameter: 120; max(3, 4) still calls the global function: 4
120
$ cd strict
$ godot --headless --no-header --check-only --script scope.gd
SCRIPT ERROR: Parse Error: The local variable "health" is shadowing an already-declared variable at line 4 in the current class. (Warning treated as error.)
          at: GDScript::reload (res://scope.gd:15)
ERROR: Failed to load script "res://scope.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --no-header --check-only --script shadow_global.gd
SCRIPT ERROR: Parse Error: The function parameter "max" has the same name as a built-in function. (Warning treated as error.)
          at: GDScript::reload (res://shadow_global.gd:4)
ERROR: Failed to load script "res://shadow_global.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scope_session.py

take_damage() changed the member health, so the change lasted after the call: 70, then 0. shadowed() declared a local called health, which shadowing: Declaring a local variable or parameter with the name of a member or a global, which hides it inside the function: the name then means the local. The editor warns about it. the member: inside the function the name means the local, 90, and the member stayed 0. self.health reaches past the local to the member.

Key idea

GDScript looks a name up in the locals first, then the members, then the globals, and the first match wins, so a local or parameter with a member’s name hides the member until the call returns.

In shadow_global.gd the name max meant two things at once: the parameter, 120, as a value, and still the global function in the call max(3, 4). The default project ran both scripts without a word; the strict project, with the two shadowing warnings at level 2, refused them with the editor’s messages.

A parameter with a member's name

func boost(speed: float): speed *= 1.5 changes the parameter, not the member speed, so the boost is lost when the call returns. Give the parameter its own name, such as factor, and write speed *= factor.

Checkpoint

Check: Parameters, defaults and return types · Local, member and global names

  1. Question 1 of 2 roll_loot(level: int, luck: float = 1.0, guaranteed: bool = false) returns level × 10, times luck, rounded, or double when guaranteed. What does roll_loot(4, 2.0) return?

    Type a number.

    A hint

    Which defaults does the call replace, and which does it keep?

    Show the answer: Question 1 of 2

    Answer: 80 gold

    The call replaces luck with 2.0 and keeps guaranteed false: 4 × 10 = 40, times 2.0 is 80. roll_loot(3, 1.5) gave 45 in the run the same way.

  2. Question 2 of 2 A script has var coins := 10 and func add(coins: int) -> void: coins += 5. After add(coins), what is the member coins?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 10, because the function changed its parameter, which hides the member

    Locals and parameters are found before members. Name the parameter amount and write coins += amount, or use self.coins.

A static function needs no instance, and a recursive one needs a way to stop

A static function: A function declared static func. It belongs to the script itself rather than to one object made from it, so it cannot use instance members or self, only its arguments, constants and static variables., declared static func, belongs to the script itself rather than to one object made from it, so it can be called without one. In exchange, it cannot use anything that belongs to one object: the reference rules out the instance’s member variables and self. A recursion: A function calling itself on a smaller part of the problem, until a base case it answers without calling itself. In Godot 4.7.2 a recursion stopped at depth 2,047 with Stack overflow. function calls itself, which suits data that holds smaller copies of itself, such as chests inside chests. Step through count_gold():

Interactive · message by message count_gold() opens a chest inside a chest Step through the calls. Each column is one call of count_gold(), waiting for the call to its right to return.
count_gold() opens a chest inside a chest, step 6 of 6. Messages so far: outer chest to middle chest: count_gold([10, …]); middle chest to inner chest: count_gold([20, 1]); inner chest to middle chest: returns 21; middle chest to outer chest: returns 31.outer chest[5, [10, [20, 1]], 3, []]middle chest[10, [20, 1]]inner chest[20, 1]count_gold([10, …])count_gold([20, 1])returns 21returns 31

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The outer chest. count_gold(treasure) starts with total 0 and adds the first thing, 5 gold coins: total 5.
  2. A chest inside. The next thing is an Array, a chest, so the function calls itself on it and waits for the answer.
  3. Deeper. The middle chest adds 10, then finds another chest and calls count_gold() again. Three calls are now running, each with its own total.
  4. The base case. The inner chest holds only coins, 20 and 1, so it makes no further call: it returns 21. A recursion needs such a case, or it never stops.
  5. Back up. The middle chest adds the 21 to its 10 and returns 31 to the outer chest.
  6. The rest. The outer chest now has 5 + 31 = 36, adds 3, and calls count_gold([]) for the empty chest, which returns 0 at once. It returns 39, as the run printed.

chests.gd: a static helper and a recursive count GD · chests.gd
extends SceneTree

# A static helper, and a recursive function that counts the gold in chests inside chests.
const MAX_LEVEL := 50
var gold_found := 0


static func clamp_level(level: int) -> int:
	return clampi(level, 1, MAX_LEVEL)   # a constant belongs to the script, so a static function may use it


func count_gold(chest: Array) -> int:
	var total := 0
	for thing in chest:
		if thing is Array:
			total += count_gold(thing)   # a chest inside the chest: the function calls itself
		else:
			total += thing
	return total


func _initialize() -> void:
	print("clamp_level(99) = ", clamp_level(99), ", clamp_level(-4) = ", clamp_level(-4))
	var treasure := [5, [10, [20, 1]], 3, []]
	gold_found = count_gold(treasure)
	print("gold in ", treasure, " = ", gold_found)
	quit()
static_mistake.gd: a static function that reads a member GD · static_mistake.gd
extends SceneTree

var gold := 100


static func spend(cost: int) -> int:
	return gold - cost


func _initialize() -> void:
	print(spend(5))
	quit()
overflow.gd: a recursion with no base case GD · overflow.gd
extends SceneTree

# A recursive function with no base case: every call makes another one, until Godot stops it.
var deepest := 0


func dive(depth: int) -> int:
	deepest = depth
	return dive(depth + 1)


func _initialize() -> void:
	quit()
	dive(1)


func _finalize() -> void:
	print("deepest call reached: ", deepest)
Static and recursive functions, and a stack overflow, Godot 4.7.2 Python · recursion_session.py
from session import files, run

# overflow.gd prints more than three thousand lines, so the session shows its first three, counts the lines of its
# backtrace and the errors that follow them, and shows its last line.
files("project.godot", "chests.gd", "static_mistake.gd", "overflow.gd")
run("""
godot --headless --no-header --script chests.gd
!godot --headless --no-header --check-only --script static_mistake.gd
!godot --headless --no-header --script overflow.gd 2>&1 | head -n 3
godot --headless --no-header --script overflow.gd 2>&1 | grep -c "] dive"
godot --headless --no-header --script overflow.gd 2>&1 | grep -c "Stack underflow"
godot --headless --no-header --script overflow.gd 2>&1 | tail -n 1
""")

Output

$ godot --headless --no-header --script chests.gd
clamp_level(99) = 50, clamp_level(-4) = 1
gold in [5, [10, [20, 1]], 3, []] = 39
$ godot --headless --no-header --check-only --script static_mistake.gd
SCRIPT ERROR: Parse Error: Cannot access non-static variable "gold" from the static function "spend()".
          at: GDScript::reload (res://static_mistake.gd:7)
ERROR: Failed to load script "res://static_mistake.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --no-header --script overflow.gd 2>&1 | head -n 3
SCRIPT ERROR: Stack overflow. Check for infinite recursion in your script.
          at: dive (res://overflow.gd:7)
          GDScript backtrace (most recent call first):
$ godot --headless --no-header --script overflow.gd 2>&1 | grep -c "] dive"
1023
$ godot --headless --no-header --script overflow.gd 2>&1 | grep -c "Stack underflow"
1024
$ godot --headless --no-header --script overflow.gd 2>&1 | tail -n 1
deepest call reached: 2047

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 recursion_session.py

clamp_level() needs only its parameter and the constant MAX_LEVEL, so it can be static; static_mistake.gd tried to read the member gold from a static function and was refused. count_gold() found 39 gold in chests nested three deep, because every call that meets a chest makes one more call, and a chest of coins only, the base case, makes none.

overflow.gd has no base case. Every call made another one until, at depth 2,047, Godot stopped with Stack overflow. Check for infinite recursion in your script. The session shows the first three lines of the output and counts the rest: a backtrace that lists 1,023 calls of dive(), then 1,024 errors that read Stack underflow! (Engine Bug), all from the one missing base case. Data can be that deep without any bug: a recursive flood fill on an open 64 by 64 map of 4,096 tiles can need a chain of up to 4,096 waiting calls, twice the limit. When the depth could grow with the data, write the function as a loop over a list of work still to do; a loop has no such limit.

Static for helpers, recursion for nested data

Make a function static when it works only on its arguments and constants, like clamp_level(). Use recursion when the data nests and stays shallow, as chests in chests do, and a loop when the depth could run into the thousands.

A rest parameter takes any number of arguments

Since Godot 4.5 a function can end with a rest parameter: The last parameter of a function, written ...name: Array, which collects every argument after the fixed ones into an array. New in Godot 4.5; it takes no default value and no typed array., written ...rest: Array, which collects every argument after the fixed ones into an array. A function with one is a variadic function: A function that takes any number of arguments, such as print() and max(). A GDScript function becomes one with a rest parameter, in Godot 4.5 and later., as print() and max() are. GDScript has no spread syntax to pass an array as separate arguments; callv() does that job.

Timeline · 3 events in order Function features by Godot version Walk through the releases. Code that uses a feature needs at least the release that brought it.
  1. 4.3

    Built-in functions as Callables new

    Utility functions such as print() and methods can be used like a Callable value, so print.callv(values) prints the items of an array as separate arguments.

  2. 4.5

    Variadic functions new syntax

    A function can end with a rest parameter, written ...name: Array, that collects every extra argument. GDScript still has no spread syntax for calls; callv() takes an array.

  3. 4.7

    The version of this lesson

    Godot 4.7.2 ran every example here, including the rest-parameter rules: one rest parameter, last, with no default value and no typed array.

The timeline has 3 events, from 4.3 to 4.7.

variadic.gd: total() and log_event(), called four ways GD · variadic.gd
extends SceneTree

# A variadic function collects the extra arguments into an Array (Godot 4.5 and later).
func total(first: float, ...rest: Array) -> float:
	var sum := first
	for value in rest:
		sum += value
	return sum


func log_event(kind: String, ...details: Array) -> void:
	print(kind, ": ", details.size(), " details ", details)


func _initialize() -> void:
	print("total(1, 2, 3) = ", total(1, 2, 3))
	print("total(4.5) = ", total(4.5))
	log_event("pickup", "coin", 5)
	log_event("save")
	var scores := [10, 20, 30]
	print("total.callv([10, 20, 30]) = ", total.callv(scores))
	print("callv(\"total\", [7, 8]) = ", callv("total", [7, 8]))
	print.callv(["print", ".callv", " takes ", 3, " pieces"])
	print("max(3, 9, 4) = ", max(3, 9, 4), ", min(2.5, 1) = ", min(2.5, 1))
	quit()
Variadic functions, callv() and the rules of the rest parameter, Godot 4.7.2 Python · variadic_session.py
from session import files, run

files("project.godot", "variadic.gd", "rest_mistakes=.")
run("""
godot --headless --no-header --script variadic.gd
!godot --headless --no-header --check-only --script two_rests.gd
!godot --headless --no-header --check-only --script rest_default.gd
!godot --headless --no-header --check-only --script typed_rest.gd
""")

Output

$ godot --headless --no-header --script variadic.gd
total(1, 2, 3) = 6.0
total(4.5) = 4.5
pickup: 2 details ["coin", 5]
save: 0 details []
total.callv([10, 20, 30]) = 60.0
callv("total", [7, 8]) = 15.0
print.callv takes 3 pieces
max(3, 9, 4) = 9, min(2.5, 1) = 1
$ godot --headless --no-header --check-only --script two_rests.gd
SCRIPT ERROR: Parse Error: Cannot have parameters after the rest parameter.
          at: GDScript::reload (res://two_rests.gd:3)
ERROR: Failed to load script "res://two_rests.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --no-header --check-only --script rest_default.gd
SCRIPT ERROR: Parse Error: The rest parameter cannot have a default value.
          at: GDScript::reload (res://rest_default.gd:3)
ERROR: Failed to load script "res://rest_default.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --no-header --check-only --script typed_rest.gd
SCRIPT ERROR: Parse Error: Typed arrays are currently not supported for the rest parameter.
          at: GDScript::reload (res://typed_rest.gd:3)
ERROR: Failed to load script "res://typed_rest.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 variadic_session.py

total(1, 2, 3) put 1 in first and [2, 3] in rest, and returned 6.0; total(4.5) left rest empty. log_event("save") shows that the rest parameter may stay empty. total.callv(scores) passed the three scores as three arguments: total without brackets is the function as a value, a Callable: A function as a value. A method's name without brackets, such as total, gives one; call() runs it with arguments and callv() with an array of them., and callv() calls it with an array of arguments. print.callv([…]) does the same for a built-in function, which works since Godot 4.3.

The three refused files are the rules of the rest parameter: only one, and it must be last; it cannot have a default value; and its type must be a plain Array, because typed arrays are not supported for it yet.

Checkpoint

Check: Static and recursive functions · Rest parameters and callv()

  1. Question 1 of 2 Which function can be static, given the members var gold := 100 and const MAX_LEVEL := 50?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: func cap_level(level: int) -> int: return mini(level, MAX_LEVEL)

    The reference says a static function has no access to instance member variables or self. Constants are fine, because they belong to the script, as MAX_LEVEL does for clamp_level() in chests.gd.

  2. Question 2 of 2 func log_event(kind: String, ...details: Array). What does details hold for log_event("hit", "bat", 12, true)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ["bat", 12, true]

    log_event("pickup", "coin", 5) in the run printed 2 details, ["coin", 5]; with three extra arguments there are three.

Practise: a levelling system

Write the three functions of levels.gd, the starter file below: the XP a level needs, the level a total of XP reaches, and a variadic function that applies bonuses one after another. The rules are in the comments.

As with the earlier tasks of this module, the two scripts go into a fresh folder beside an empty project.godot. Write the three functions, then call godot --headless --no-header --script check_levels.gd there and read the last line it prints.

levels.gd: the starter file GD · practice/levels.gd
extends Node

# Your task: three functions for a levelling system.
const MAX_LEVEL := 50


# The total XP needed to reach a level: level 1 needs 0, and each next level needs 100 × (the level before it) more,
# so level 2 needs 100, level 3 needs 300 and level 4 needs 600. Below 1 counts as 1; above MAX_LEVEL as MAX_LEVEL.
func xp_for_level(level: int) -> int:
	return 0  # replace this line


# The highest level whose XP need is at most xp, from 1 to MAX_LEVEL: 99 XP is level 1, 100 XP is level 2.
func level_from_xp(xp: int) -> int:
	return 1  # replace this line


# The base value with every bonus applied in turn, each a fraction: 0.1 means 10 % more, -0.5 means half.
# combine_bonuses(100.0, 0.1, 0.2) is 100 × 1.1 × 1.2 = 132.0, and with no bonuses the base comes back.
func combine_bonuses(base: float, ...bonuses: Array) -> float:
	return base  # replace this line
check_levels.gd: the checker of this task GD · practice/check_levels.gd
extends SceneTree

# Checks levels.gd. Run it in the project's folder: godot --headless --no-header --script check_levels.gd
const XP_FOR := [[1, 0], [2, 100], [3, 300], [4, 600], [50, 122500], [0, 0], [-3, 0], [99, 122500]]
const LEVEL_FROM := [[0, 1], [99, 1], [100, 2], [299, 2], [300, 3], [122500, 50], [9999999, 50], [-40, 1]]


func _initialize() -> void:
	var levels: Node = load("res://levels.gd").new()
	var passed := 0
	var total := 0
	for case in XP_FOR:
		var got: int = levels.xp_for_level(case[0])
		total += 1
		passed += int(got == case[1])
		print("%s xp_for_level(%d) = %d (want %d)" % ["PASS" if got == case[1] else "FAIL", case[0], got, case[1]])
	for case in LEVEL_FROM:
		var got: int = levels.level_from_xp(case[0])
		total += 1
		passed += int(got == case[1])
		print("%s level_from_xp(%d) = %d (want %d)" % ["PASS" if got == case[1] else "FAIL", case[0], got, case[1]])
	for case in [[[100.0, 0.1, 0.2], 132.0], [[80.0], 80.0], [[50.0, -0.5, 0.5], 37.5]]:
		var got: float = levels.combine_bonuses.callv(case[0])
		total += 1
		var ok := is_equal_approx(got, case[1])
		passed += int(ok)
		print("%s combine_bonuses(%s) = %s (want %s)" % ["PASS" if ok else "FAIL", ", ".join(case[0].map(str)), got, case[1]])
	levels.free()
	print("%d of %d checks passed" % [passed, total])
	quit(0 if passed == total else 1)

The session puts a correct levels.gd and a plausible wrong one through the checker:

The checker on a right answer and on a tempting one, Godot 4.7.2 Python · check_session.py
from session import files, run

# The checker on a levels.gd that solves the task, then on a tempting one with two mistakes.
files("practice/project.godot=right/project.godot", "practice/check_levels.gd=right/check_levels.gd",
      "practice/solution/levels.gd=right/levels.gd",
      "practice/project.godot=tempting/project.godot", "practice/check_levels.gd=tempting/check_levels.gd",
      "practice/tempting/levels.gd=tempting/levels.gd")
run("""
cd right
godot --headless --no-header --script check_levels.gd
cd ../tempting
!godot --headless --no-header --script check_levels.gd
""")

Output

$ cd right
$ godot --headless --no-header --script check_levels.gd
PASS xp_for_level(1) = 0 (want 0)
PASS xp_for_level(2) = 100 (want 100)
PASS xp_for_level(3) = 300 (want 300)
PASS xp_for_level(4) = 600 (want 600)
PASS xp_for_level(50) = 122500 (want 122500)
PASS xp_for_level(0) = 0 (want 0)
PASS xp_for_level(-3) = 0 (want 0)
PASS xp_for_level(99) = 122500 (want 122500)
PASS level_from_xp(0) = 1 (want 1)
PASS level_from_xp(99) = 1 (want 1)
PASS level_from_xp(100) = 2 (want 2)
PASS level_from_xp(299) = 2 (want 2)
PASS level_from_xp(300) = 3 (want 3)
PASS level_from_xp(122500) = 50 (want 50)
PASS level_from_xp(9999999) = 50 (want 50)
PASS level_from_xp(-40) = 1 (want 1)
PASS combine_bonuses(100.0, 0.1, 0.2) = 132.0 (want 132.0)
PASS combine_bonuses(80.0) = 80.0 (want 80.0)
PASS combine_bonuses(50.0, -0.5, 0.5) = 37.5 (want 37.5)
19 of 19 checks passed
$ cd ../tempting
$ godot --headless --no-header --script check_levels.gd
PASS xp_for_level(1) = 0 (want 0)
PASS xp_for_level(2) = 100 (want 100)
PASS xp_for_level(3) = 300 (want 300)
PASS xp_for_level(4) = 600 (want 600)
PASS xp_for_level(50) = 122500 (want 122500)
PASS xp_for_level(0) = 0 (want 0)
PASS xp_for_level(-3) = 0 (want 0)
PASS xp_for_level(99) = 122500 (want 122500)
PASS level_from_xp(0) = 1 (want 1)
PASS level_from_xp(99) = 1 (want 1)
FAIL level_from_xp(100) = 1 (want 2)
PASS level_from_xp(299) = 2 (want 2)
FAIL level_from_xp(300) = 2 (want 3)
FAIL level_from_xp(122500) = 49 (want 50)
PASS level_from_xp(9999999) = 50 (want 50)
PASS level_from_xp(-40) = 1 (want 1)
FAIL combine_bonuses(100.0, 0.1, 0.2) = 130.0 (want 132.0)
PASS combine_bonuses(80.0) = 80.0 (want 80.0)
FAIL combine_bonuses(50.0, -0.5, 0.5) = 50.0 (want 37.5)
14 of 19 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_session.py

The tempting answer compares with < where the rule says “at most”, so exactly 100 XP stays at level 1, and it adds the bonuses together instead of applying them in turn: 100 × (1 + 0.1 + 0.2) is 130, not the 132 the rule asks for, and a halving followed by +50 % cancels out instead of giving 37.5. Your output should end with 19 of 19 checks passed. Then take the self-check on this page’s four ideas: a miss returns later in the round, and a note under each option says what it gets right or leaves out:

Practice that adapts to you

Practise this lesson

12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 Why is func spawn(count: int = 1, kind: String) refused?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: Parameters with defaults must come after every parameter without one

    Arguments are matched by position, so the optional ones must be at the end: func spawn(kind: String, count: int = 1).

  2. Question 2 of 12 Which version of rank() loads without the error "Not all code paths return a value"?

    Read the code, then choose one answer.

    # rank(score: int) -> String
    Show the answer: Question 2 of 12

    Answer: if score >= 90: return "gold" then elif score >= 50: return "silver" then else: return "bronze"

    A typed function needs a return on every path. An else branch, or a return at the end of the function, gives Godot one it can see.

  3. Question 3 of 12 announce(item: String) -> void prints a line. A teammate writes var said = announce("a map"). What happens?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: The script does not load: the result of a void function cannot be used

    void means the function returns nothing to use. Call it on a line of its own, or give it a return type and a return value if the caller needs one.

  4. Question 4 of 12 In which order does GDScript look up a name used inside a function?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Local, then class member, then global

    Locals and parameters first, then the script's members, then global names such as print() and Node2D.

  5. Question 5 of 12 take_damage() declares var left := health - amount. What happens to left when the call returns?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: It is gone; the next call gets a new left

    Keep values that must last between calls, such as health, in members; keep working values of one call in locals.

  6. Question 6 of 12 shadow_global.gd names a parameter max. Inside that function, what are max and max(3, 4)?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: max is the parameter, 120, while max(3, 4) still calls the global function and gives 4

    Avoid names of global functions and classes for variables and parameters: the strict project reported "The function parameter "max" has the same name as a built-in function."

  7. Question 7 of 12 What is the base case of a recursive function?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: The case it answers without calling itself, which ends the recursion

    Without a base case every call makes another one, as overflow.gd did until Godot stopped it at depth 2,047.

  8. Question 8 of 12 At what depth did Godot 4.7.2 stop the recursion of overflow.gd?

    Type a number.

    Show the answer: Question 8 of 12

    Answer: 2047 calls deep

    The run printed "deepest call reached: 2047" after "Stack overflow. Check for infinite recursion in your script."

  9. Question 9 of 12 A pathfinding helper explores a 100 × 100 grid by calling itself for each neighbour. What is the safer design?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: A loop over a list of cells still to visit, because the recursion could nest past the stack limit

    When the depth of a recursion grows with the data, move the work to a list and a loop: the depth stays one call, whatever the size.

  10. Question 10 of 12 Which declaration does Godot 4.7.2 accept?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: func spawn(kind: String, ...positions: Array) -> void:

    The session showed the three refused forms with their messages.

  11. Question 11 of 12 func total(first: float, ...rest: Array) -> float adds its arguments. What does total.callv([2, 4, 6, 8]) return?

    Type a number.

    Show the answer: Question 11 of 12

    Answer: 20

    callv() passes the array's items as separate arguments: first is 2 and rest is [4, 6, 8], so the total is 20.0.

  12. Question 12 of 12 bonuses is an Array of floats. How do you pass its items to combine_bonuses(base, ...bonuses) as separate arguments?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: combine_bonuses.callv([base] + bonuses)

    The checker of the practice task calls combine_bonuses.callv(case[0]) for exactly this reason.

Interview questions

Warm-up: what is a variadic function in GDScript, and how do you call one with values that are in an array?

A variadic function takes any number of arguments. Since Godot 4.5 its last parameter can be a rest parameter, such as ...rest: Array, which collects every argument after the fixed ones into an array: total(1, 2, 3) with func total(first: float, ...rest: Array) gets first 1 and rest [2, 3]. There can be only one rest parameter, it must be last, it cannot have a default and it cannot be a typed array. GDScript has no spread syntax, so to pass an array’s items as separate arguments, call the function through its Callable: total.callv([10, 20, 30]) gave 60.0. Built-in functions work the same way since Godot 4.3, as in print.callv(values).

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A level editor's fill tool colours connected tiles with a recursive fill(x, y) that calls itself for the four neighbours. It works in small rooms, but filling an open 64 by 64 area prints "Stack overflow" and leaves some tiles uncoloured. Explain the limit with numbers, and rewrite the fill so it works on any map.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A script has var speed := 200.0 and func boost(speed: float) -> void: speed *= 1.5. After boost(speed) the player is no faster, and no error appears. Explain what happens, fix it, and say which project settings would have flagged it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A function declares typed parameters, defaults at the end, and a return type after ->; Godot refuses a missing return path, a wrong argument count or type and the use of a void result before the game runs.
  • Names are looked up in the locals, then the members, then the globals, so a local or parameter with a member’s name shadows it; the editor warns, and a strict project refuses it.
  • A static function belongs to the script and cannot use instance members, and a recursive function needs a base case; without one Godot stopped at depth 2,047 with “Stack overflow”.
  • A rest parameter, ...rest: Array, collects any number of extra arguments (Godot 4.5 and later), and callv() passes an array’s items as arguments.

Terms in this lesson

Callable
A function as a value. A method's name without brackets, such as total, gives one; call() runs it with arguments and callv() with an array of them.
recursion
A function calling itself on a smaller part of the problem, until a base case it answers without calling itself. In Godot 4.7.2 a recursion stopped at depth 2,047 with Stack overflow.
rest parameter
The last parameter of a function, written ...name: Array, which collects every argument after the fixed ones into an array. New in Godot 4.5; it takes no default value and no typed array.
shadowing
Declaring a local variable or parameter with the name of a member or a global, which hides it inside the function: the name then means the local. The editor warns about it.
static function
A function declared static func. It belongs to the script itself rather than to one object made from it, so it cannot use instance members or self, only its arguments, constants and static variables.
variadic function
A function that takes any number of arguments, such as print() and max(). A GDScript function becomes one with a rest parameter, in Godot 4.5 and later.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.