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

Variables, types and operators

Declare GDScript variables and constants with static types and :=, predict integer division and modulo, convert values safely and compare floats correctly.

  • 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 variables and constants with and without static types, including := inference
  • Predict the results of integer division, modulo with negative numbers and mixed int and float maths
  • Use literals, bit operators and the limits of a 64-bit int in game data
  • Convert between int, float, String and bool without losing data by surprise
  • Compare floats with is_equal_approx instead of ==

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare variables and constants with and without static types, including := inference
  • Predict the results of integer division, modulo with negative numbers and mixed int and float maths
  • Use literals, bit operators and the limits of a 64-bit int in game data
  • Convert between int, float, String and bool without losing data by surprise
  • Compare floats with is_equal_approx instead of ==

Before you start

On this page

A critical hit should deal 1.5 times the damage, yet 17 * 3 / 2 gives 25 where the designer’s table says 26, and the running game says nothing about the lost half point. GDScript has two kinds of number, int and float, and every operator keeps or drops information by rules you can learn in an afternoon.

Below you declare variables with and without types, predict what division, remainders and powers return, store flags in the bits of one int, convert between numbers, text and true or false without surprises, and compare floats the way Godot’s own documentation recommends. Every result on this page was printed by Godot 4.7.2.

A declaration says what a variable may hold

var creates a variable. Written without a type, the variable may hold any kind of value, now an int and later a String. Give it a type and Godot holds every later line to it: this is a static type: A type fixed for a variable when it is declared, as in var hp: int or var hp := 100. Godot then refuses any line that would store another type, with a parse error before the script runs., written var hp: int = 75. Writing := instead of = lets Godot take the type from the value, which is called type inference: Letting Godot take a variable's type from its first value, written :=. var speed := 4.5 makes speed a float for good, and var steps := 4 makes steps an int.: var speed := 4.5 makes speed a float as surely as var speed: float = 4.5. const declares a value that never changes and must be known when the script is read.

Step through four declarations and the lines that try to change them:

Interactive · step by step Four declarations, and what each one allows later Step through the statements in order. The sub-label under each name says what it holds now.
Four declarations, and what each one allows later, step 8 of 8. Showing var loot = 3 (holds a String now), var hp: int = 75 (holds 100, int only), var speed := 4.5 (holds 4.5, float only), const MAX_HP := 100 (holds 100, for good). In focus: var hp: int = 75.var loot = 3holds a String nowvar hp: int = 75holds 100, int onlyvar speed := 4.5holds 4.5, float onlyconst MAX_HP := 100holds 100, for good

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

Every step as text
  1. No type. var loot = 3 gives loot the value 3, an int, but no type of its own: the variable may hold any kind of value later.
  2. A type, written out. var hp: int = 75 declares hp as an int. From now on Godot refuses, before the game runs, any line that would put something else in it.
  3. A type, inferred. var speed := 4.5 makes the same promise as var speed: float = 4.5. The := takes the type from the value, here a float.
  4. A constant. const MAX_HP := 100 is a constant. Its value must be known when the script is read, and no line may change it.
  5. loot changes kind. loot = "three gems" is allowed, because loot has no type. The run printed that loot now holds a String.
  6. hp refuses a String. hp = "full" stops the whole script from loading: Cannot assign a value of type "String" as "int". The mistake is found before any player sees it.
  7. The constant refuses. MAX_HP = 120 is refused the same way: Cannot assign a new value to a constant.
  8. A valid change. hp = MAX_HP is fine: an int into an int. The run printed hp = 100 of 100.

The figure follows these two scripts. The first runs; the second holds the two refused lines, so Godot only parses it, with --check-only:

declarations.gd: four declarations GD · declarations.gd
extends SceneTree

# Four ways to declare a name, and what each one holds.
const MAX_HP := 100


func _initialize() -> void:
	var loot = 3                  # no type: the variable can hold anything
	var hp: int = 75              # a static type, written out
	var speed := 4.5              # a static type, inferred from 4.5
	var player_name := "Asha"     # inferred: String
	print("loot is ", type_string(typeof(loot)), ", hp is ", type_string(typeof(hp)), ", speed is ",
			type_string(typeof(speed)), ", player_name is ", type_string(typeof(player_name)))
	loot = "three gems"           # allowed: loot has no type
	print("loot now holds ", type_string(typeof(loot)), ": ", loot)
	hp = MAX_HP
	print("hp = ", hp, " of ", MAX_HP)
	var steps := 4                # inferred from 4: an int, for good
	steps = 4.5                   # a float into an int variable keeps the whole part
	print("steps = ", steps, ", still an ", type_string(typeof(steps)))
	quit()
typed_mistakes.gd: two lines a type refuses GD · typed_mistakes.gd
extends SceneTree

const MAX_HP := 100


func _initialize() -> void:
	var hp: int = 75
	hp = "full"
	MAX_HP = 120
	quit()
The declarations run, and the mistakes refused, Godot 4.7.2 Python · types_session.py
from session import files, run

files("project.godot", "declarations.gd", "typed_mistakes.gd")
run("""
godot --headless --no-header --script declarations.gd
!godot --headless --no-header --check-only --script typed_mistakes.gd
""")

Output

$ godot --headless --no-header --script declarations.gd
loot is int, hp is int, speed is float, player_name is String
loot now holds String: three gems
hp = 100 of 100
steps = 4, still an int
$ godot --headless --no-header --check-only --script typed_mistakes.gd
SCRIPT ERROR: Parse Error: Cannot assign a value of type "String" as "int".
          at: GDScript::reload (res://typed_mistakes.gd:8)
SCRIPT ERROR: Parse Error: Value of type "String" cannot be assigned to a variable of type "int".
          at: GDScript::reload (res://typed_mistakes.gd:8)
SCRIPT ERROR: Parse Error: Cannot assign a new value to a constant.
          at: GDScript::reload (res://typed_mistakes.gd:9)
ERROR: Failed to load script "res://typed_mistakes.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 types_session.py

type_string(typeof(x)) names the type a value has right now. The untyped loot went from int to String without complaint, which is convenient in a five-line experiment and a source of bugs in a game: nothing stops a later line from storing text where the rest of the code expects a number. The typed hp turned that mistake into a parse error with a line number, found before the script ran at all. The documentation puts that first among the benefits of static types; better completion in the editor and faster code are among the others.

Names follow the style guide: snake_case for variables, such as player_name, and CONSTANT_CASE for constants, such as MAX_HP. Use := where the value makes the type obvious, and write the type out where it does not, as in var hits: int = data["hits"]; either way a typed variable turns a whole class of bugs into parse errors.

Inferring an int when you meant a float

var steps := 4 infers int from the 4, so the later steps = 4.5 stores 4, as the last line of the run shows. Write var steps := 4.0, or var steps: float = 4, when the variable will ever hold a fraction.

Integer division drops the fraction, and the remainder keeps the sign

When both sides of / are ints, GDScript performs integer division: Dividing an int by an int, which gives an int with the fraction dropped towards zero: 7 / 2 is 3 and -7 / 2 is -3. With a float on either side, such as 7 / 2.0, the result is 3.5.: the result is an int, and the fraction is dropped, towards zero. Make either side a float and the result is a float. % gives the remainder, and its sign follows the number being divided:

division.gd: division, remainders and powers GD · division.gd
extends SceneTree

# Division and remainders with ints, floats and negative numbers.
func _initialize() -> void:
	print("7 / 2 = ", 7 / 2, "    -7 / 2 = ", -7 / 2, "    7 / 2.0 = ", 7 / 2.0, "    7.0 / 2 = ", 7.0 / 2)
	print("7 % 3 = ", 7 % 3, "    -7 % 3 = ", -7 % 3, "    posmod(-7, 3) = ", posmod(-7, 3))
	print("fmod(-7.5, 3.0) = ", fmod(-7.5, 3.0), "    fposmod(-7.5, 3.0) = ", fposmod(-7.5, 3.0))
	print("2 ** 10 = ", 2 ** 10, "    2 ** 0.5 = ", 2 ** 0.5, "    2 ** 2 ** 3 = ", 2 ** 2 ** 3)
	# A tile grid 5 wide: which column is tile 13 in, and which row? And the column one step left of column 0?
	var tile := 13
	print("tile 13: row ", tile / 5, ", column ", tile % 5)
	print("one column left of 0: ", (0 - 1) % 5, " with %, ", posmod(0 - 1, 5), " with posmod")
	quit()
damage_math.gd: a critical hit worth 1.5 times, three ways GD · damage_math.gd
extends SceneTree

# Damage after armour, with a critical hit worth 1.5 times the rest, worked out three ways.
const HITS := [[25, 8], [11, 0], [3, 2], [4, 7]]


func _initialize() -> void:
	print("base armor  left | left * 3 / 2 | int(left * 1.5) | roundi(left * 1.5)")
	for hit in HITS:
		var left: int = hit[0] - hit[1]
		print("%4d %5d %5d | %12d | %15d | %18d" % [hit[0], hit[1], left, left * 3 / 2, int(left * 1.5), roundi(left * 1.5)])
	quit()
Division and the damage table, Godot 4.7.2 Python · division_session.py
from session import files, run

files("project.godot", "division.gd", "damage_math.gd")
run("""
godot --headless --no-header --script division.gd
godot --headless --no-header --script damage_math.gd
""")

Output

$ godot --headless --no-header --script division.gd
7 / 2 = 3    -7 / 2 = -3    7 / 2.0 = 3.5    7.0 / 2 = 3.5
7 % 3 = 1    -7 % 3 = -1    posmod(-7, 3) = 2
fmod(-7.5, 3.0) = -1.5    fposmod(-7.5, 3.0) = 1.5
2 ** 10 = 1024    2 ** 0.5 = 1.4142135623731    2 ** 2 ** 3 = 64
tile 13: row 2, column 3
one column left of 0: -1 with %, 4 with posmod
$ godot --headless --no-header --script damage_math.gd
base armor  left | left * 3 / 2 | int(left * 1.5) | roundi(left * 1.5)
  25     8    17 |           25 |              25 |                 26
  11     0    11 |           16 |              16 |                 17
   3     2     1 |            1 |               1 |                  2
   4     7    -3 |           -4 |              -4 |                 -5

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

-7 / 2 is -3, not -4: the fraction goes towards zero, also for negative numbers. -7 % 3 is -1 for the same reason, which matters whenever a remainder is used as a position. The tile grid of the run is 5 tiles wide: tile 13 sits in row 13 / 5, which is 2, and column 13 % 5, which is 3. Step one column left of column 0 and % gives -1, a column that does not exist, while posmod() wraps round to 4. For floats, fmod() and fposmod() do the same two jobs. And ** raises to a power, working from the left: 2 ** 2 ** 3 is (2 ** 2) ** 3, 64.

Key idea

An int divided by an int is an int with the fraction dropped towards zero, and % takes the sign of the number divided; use a float, roundi() or posmod() when you need anything else.

The damage table shows what dropping a fraction costs. Step through its first row, 25 damage against 8 armour, then its last:

Interactive · step by step A critical hit on 17, worked out three ways Step through the three columns of the damage table for 17, then for -3. Each box is one operation; its sub-label is the result for the row on screen.
A critical hit on 17, worked out three ways, step 7 of 7. Showing left = base - armor (4 - 7 = -3, an int), left * 3 (-9, an int), left * 3 / 2 (-9 / 2 = -4, an int), left * 1.5 (-4.5, a float), int(left * 1.5) (int(-4.5) = -4), roundi(left * 1.5) (roundi(-4.5) = -5). In focus: left * 3 / 2, int(left * 1.5), roundi(left * 1.5).left = base - armor4 - 7 = -3, an intleft * 3-9, an intleft * 3 / 2-9 / 2 = -4, an intleft * 1.5-4.5, a floatint(left * 1.5)int(-4.5) = -4roundi(left * 1.5)roundi(-4.5) = -5

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

Every step as text
  1. An int to start. Armour 8 takes 25 down to 17. Both are ints, so left is an int.
  2. Times 3. left * 3 is 51. An int times an int is an int.
  3. The fraction goes. 51 / 2 divides two ints, so it is integer division: 25, and the .5 is dropped without a word. A critical hit lost half a point.
  4. A float instead. left * 1.5 mixes an int with a float, and the result is a float: 25.5. Nothing is lost yet.
  5. int() cuts. int(25.5) turns it back into an int by cutting off the fraction: 25 again, the same as integer division.
  6. roundi() rounds. roundi(25.5) rounds to the nearest whole number, and a half goes away from zero: 26, the 1.5 times 17 that the rules asked for.
  7. Now with -3. With armour 7 against 4, left is -3. -3 * 3 / 2 is -4, int(-4.5) is -4: both cut towards zero. roundi(-4.5) is -5, away from zero.

left * 3 / 2 and int(left * 1.5) both cut 25.5 to 25, and 16.5 to 16. Only roundi() gives 26 and 17, the numbers in the designer’s table, because it rounds to the nearest whole number with halves going away from zero, which is also why -4.5 became -5 while the other two columns gave -4.

Taking half of an int with / 2

left * 3 / 2 loses the half point of every odd number, and the running game never says so. Write roundi(left * 1.5) when you want rounding, or left / 2.0 when you want the fraction kept, so that the line says which one you meant.

Checkpoint

Check: Typed, inferred and untyped declarations · Integer division and remainders

  1. Question 1 of 2 A script declares var gold := 10 and later has the line gold = "ten". What happens?

    Choose one answer.

    A hint

    What type does := give gold?

    Show the answer: Question 1 of 2

    Answer: The script does not load; Godot reports a parse error on that line

    var gold := 10 infers int from 10, so gold is an int for good. Storing a String in it is refused with a parse error, "Cannot assign a value of type "String" as "int"".

  2. Question 2 of 2 left is the int 13. What does left * 3 / 2 give?

    Type a number.

    A hint

    Work out 13 * 3 first, then divide two ints.

    Show the answer: Question 2 of 2

    Answer: 19

    13 * 3 is 39, and 39 / 2 divides two ints, so the fraction of 19.5 is dropped: 19. roundi(13 * 1.5) would give 20.

Whole numbers have literals, bits and a 64-bit limit

An int is stored in 64 bits, so it runs from −2⁶³ to 2⁶³ − 1, a little over 9.2 × 10¹⁸ either way, and a result outside that range wraps around to the other end. Ints can be written in hexadecimal (0x1F) or binary (0b1010), and long numbers may carry underscores for reading, 1_000_000. Binary is the natural way to think about flags: one int holds many yes-or-no values, one per bit, and the bit operators read and change them.

numbers.gd: literals, bits and the int limit GD · numbers.gd
extends SceneTree

# Whole-number literals, bit operators and the limits of a 64-bit int.
func _initialize() -> void:
	print("0x1F = ", 0x1F, ", 0b1010 = ", 0b1010, ", 1_000_000 = ", 1_000_000)
	# One int can hold many yes-or-no flags, one per bit: here layers 1 and 3 are on.
	var layers := 0b0001 | 0b0100
	print("layers = ", layers, ", layer 3 on: ", layers & 0b0100 != 0, ", layer 2 on: ", layers & 0b0010 != 0)
	print("1 << 4 = ", 1 << 4, ", 256 >> 2 = ", 256 >> 2, ", 6 ^ 3 = ", 6 ^ 3, ", ~6 = ", ~6)
	var biggest := 9223372036854775807
	print("biggest int: ", biggest)
	print("biggest + 1: ", biggest + 1)
	quit()
Literals, bits and the 64-bit limit, Godot 4.7.2 Python · numbers_session.py
from session import files, run

files("project.godot", "numbers.gd")
run("""
godot --headless --no-header --script numbers.gd
""")

Output

$ godot --headless --no-header --script numbers.gd
0x1F = 31, 0b1010 = 10, 1_000_000 = 1000000
layers = 5, layer 3 on: true, layer 2 on: false
1 << 4 = 16, 256 >> 2 = 64, 6 ^ 3 = 5, ~6 = -7
biggest int: 9223372036854775807
biggest + 1: -9223372036854775808

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

Operator Does In the run
a | b sets the bits that are set in either 0b0001 | 0b0100 is 5: layers 1 and 3
a & b keeps the bits set in both 5 & 0b0100 is not 0, so layer 3 is on
a ^ b keeps the bits set in exactly one 6 ^ 3 is 5
~a flips every bit ~6 is -7
a << n, a >> n moves the bits n places left or right 1 << 4 is 16, 256 >> 2 is 64

The flags in the run are numbered the way Godot numbers its collision layers, which a later lesson builds on: layer 1 is the lowest bit. The last line is the limit. Adding 1 to the largest int gave the most negative one, with no error; a game that adds up very large numbers, such as an idle game’s gold, has to stop at a ceiling of its own before that.

Number Base Converter See any int in binary and hexadecimal, such as 5 as 0b101 or 31 as 0x1F. Programmer Calculator (Bitwise) Try &, |, ^, ~, << and >> on your own numbers before you use them as flags.

Conversions keep what fits the new type

Turning a value into another type keeps what the new type can hold and drops the rest. int() cuts a float’s fraction towards zero; roundi(), floori() and ceili() choose the direction instead. Text becomes a number with to_int() and to_float(), a number becomes text with str(), and any number becomes true or false with bool():

conversions.gd: from one type to another GD · conversions.gd
extends SceneTree

# Converting between int, float, String and bool, and what each conversion keeps.
func _initialize() -> void:
	print("int(3.9) = ", int(3.9), ", int(-3.9) = ", int(-3.9))
	print("roundi(2.5) = ", roundi(2.5), ", roundi(-2.5) = ", roundi(-2.5), ", floori(-3.5) = ", floori(-3.5),
			", ceili(-3.5) = ", ceili(-3.5))
	print("\"42\".to_int() = ", "42".to_int(), ", \"12 gems\".to_int() = ", "12 gems".to_int(),
			", \"gems\".to_int() = ", "gems".to_int(), ", \"3.9\".to_int() = ", "3.9".to_int())
	print("\"42\".is_valid_int() = ", "42".is_valid_int(), ", \"12 gems\".is_valid_int() = ",
			"12 gems".is_valid_int(), ", \"3.9\".to_float() = ", "3.9".to_float())
	print("str(42) + \"!\" = ", str(42) + "!", ", str(0.5) = ", str(0.5), ", str(true) = ", str(true))
	print("bool(0) = ", bool(0), ", bool(-2) = ", bool(-2), ", bool(0.0) = ", bool(0.0), ", int(true) = ", int(true))
	var hp: int = 7.9
	print("var hp: int = 7.9 stores ", hp)
	quit()
concat_mistake.gd: text plus a number GD · concat_mistake.gd
extends SceneTree

func _initialize() -> void:
	var score := 1250
	print("Score: " + score)
	quit()
warned.gd: two lines the editor warns about GD · warned.gd
extends SceneTree

# Two lines that run, and that the editor marks with a warning.
func _initialize() -> void:
	var hits := 7
	var half := hits / 2
	var hp: int = 7.9
	print("half = ", half, ", hp = ", hp)
	quit()
Conversions, a refused +, and warnings made errors, Godot 4.7.2 Python · conversions_session.py
from session import files, run

# The same warned.gd in a project with Godot's default warning levels, then in strict/, whose project.godot turns the
# integer_division and narrowing_conversion warnings into errors (level 2).
files("project.godot", "conversions.gd", "concat_mistake.gd", "warned.gd", "strict/project.godot", "warned.gd=strict/warned.gd")
run("""
godot --headless --no-header --script conversions.gd
!godot --headless --no-header --check-only --script concat_mistake.gd
godot --headless --no-header --script warned.gd
cat strict/project.godot
cd strict
!godot --headless --no-header --check-only --script warned.gd
""")

Output

$ godot --headless --no-header --script conversions.gd
int(3.9) = 3, int(-3.9) = -3
roundi(2.5) = 3, roundi(-2.5) = -3, floori(-3.5) = -4, ceili(-3.5) = -3
"42".to_int() = 42, "12 gems".to_int() = 12, "gems".to_int() = 0, "3.9".to_int() = 3
"42".is_valid_int() = true, "12 gems".is_valid_int() = false, "3.9".to_float() = 3.9
str(42) + "!" = 42!, str(0.5) = 0.5, str(true) = true
bool(0) = false, bool(-2) = true, bool(0.0) = false, int(true) = 1
var hp: int = 7.9 stores 7
$ godot --headless --no-header --check-only --script concat_mistake.gd
SCRIPT ERROR: Parse Error: Invalid operands "String" and "int" for "+" operator.
          at: GDScript::reload (res://concat_mistake.gd:5)
ERROR: Failed to load script "res://concat_mistake.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
$ godot --headless --no-header --script warned.gd
half = 3, hp = 7
$ cat strict/project.godot
config_version=5

[application]

config/name="Types, strict"

[debug]

gdscript/warnings/integer_division=2
gdscript/warnings/narrowing_conversion=2
$ cd strict
$ godot --headless --no-header --check-only --script warned.gd
SCRIPT ERROR: Parse Error: Integer division. Decimal part will be discarded. (Warning treated as error.)
          at: GDScript::reload (res://warned.gd:6)
SCRIPT ERROR: Parse Error: Narrowing conversion (float is converted to int and loses precision). (Warning treated as error.)
          at: GDScript::reload (res://warned.gd:7)
ERROR: Failed to load script "res://warned.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 conversions_session.py

Conversion Result Why it matters
int(-3.9) -3 cuts towards zero; floori(-3.5) is -4
roundi(-2.5) -3 halves go away from zero
"12 gems".to_int() 12 the digits are read and the rest ignored
"gems".to_int() 0 text with no number looks like a real 0
"12 gems".is_valid_int() false ask this before trusting what a player typed
bool(-2), bool(0.0) true, false only zero is false
var hp: int = 7.9 7 the fraction is dropped while the game runs

Text and numbers do not mix with +: "Score: " + score is refused as Invalid operands "String" and "int", so write "Score: " + str(score), or a format string, which the next lesson covers.

The two lines of warned.gd ran without a word: hits / 2 gave 3 and hp became 7. In the editor, the script editor’s status bar lists a warning for each, Integer division and Narrowing conversion, but a terminal run shows none. Every warning is a project setting under debug/gdscript/warnings/, and its level 2 turns it into an error, which is what strict/project.godot does for these two. --check-only then refuses the same file, with the editor’s words and “(Warning treated as error.)”.

Checkpoint

Check: Literals, bits and the int limit · Converting between types

  1. Question 1 of 2 flags is 0b0110, with layer 1 as the lowest bit. Which layers are on?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Layers 2 and 3

    Read the bits from the right: bit 1 is 0, bits 2 and 3 are 1, bit 4 is 0. flags & 0b0001 is 0, so layer 1 is off; flags & 0b0010 is not 0, so layer 2 is on.

  2. Question 2 of 2 A player types "ten" into the amount box of a shop. What does "ten".to_int() return, and what should the code do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 0, so check is_valid_int() first and refuse the text

    "gems".to_int() returned 0 in the run, the same as "0" would. Ask "ten".is_valid_int() before you use the number, and show the player a message when it is false.

Floats are close, not exact, so compare them approximately

A float is stored in 64 bits and holds about 14 reliable decimal digits, which is plenty, but not every decimal fits exactly: 0.1 and 0.2 are each stored as the nearest value that does fit, and their sum lands just above 0.3. print() rounds a float before it shows it, and the difference sits in the 17th decimal place, so it does not show, which is exactly why it surprises people:

floats.gd: 0.1 + 0.2, and the 32-bit floats of Vector2 GD · floats.gd
extends SceneTree

# 0.1 + 0.2 is not exactly 0.3, and a Vector2 keeps less precision than a float variable.
func _initialize() -> void:
	var total := 0.1 + 0.2
	print("total = ", total)
	print("total == 0.3: ", total == 0.3)
	print("is_equal_approx(total, 0.3): ", is_equal_approx(total, 0.3))
	print("17 decimals: ", "%.17f" % total)
	var v := Vector2(0.1, 0.0)
	print("Vector2(0.1, 0).x = ", v.x, ", == 0.1: ", v.x == 0.1, ", approx: ", is_equal_approx(v.x, 0.1))
	print("16777217.0 in a float: ", 16777217.0, ", in a Vector2: ", Vector2(16777217.0, 0.0).x)
	print("float(2 ** 53) == float(2 ** 53 + 1): ", float(2 ** 53) == float(2 ** 53 + 1))
	quit()
Float comparisons and Vector2 precision, Godot 4.7.2 Python · floats_session.py
from session import files, run

files("project.godot", "floats.gd")
run("""
godot --headless --no-header --script floats.gd
""")

Output

$ godot --headless --no-header --script floats.gd
total = 0.3
total == 0.3: false
is_equal_approx(total, 0.3): true
17 decimals: 0.30000000000000004
Vector2(0.1, 0).x = 0.10000000149012, == 0.1: false, approx: true
16777217.0 in a float: 16777217.0, in a Vector2: 16777216.0
float(2 ** 53) == float(2 ** 53 + 1): true

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

total printed as 0.3, yet total == 0.3 is false; 17 decimals show 0.30000000000000004. is_equal_approx(total, 0.3) is true: it allows a difference that grows with the size of the numbers, which is what the float reference recommends instead of ==.

Waiting for a float to hit an exact value

if position.x == 100.0: may never be true for a position that moves by speed * delta, because it steps over 100.0 by a tiny amount. Test is_equal_approx(position.x, 100.0), or better, position.x >= 100.0.

Vectors are coarser. Vector2 and Vector3 store their numbers as single-precision float: A 32-bit floating-point number with about 6 reliable digits, used inside Vector2, Vector3 and PackedFloat32Array. It holds every whole number only up to 16,777,216; a float variable is 64-bit., 32 bits with about 6 reliable digits, so the 0.1 in Vector2(0.1, 0) comes back as 0.10000000149012 and is not equal to the 0.1 of a float variable. A 32-bit float holds every whole number only up to 16,777,216, which is why 16,777,217 came back one less; in a world that large, a position can no longer move by less than a pixel or two. A 64-bit float runs out of exact whole numbers too, at 2 to the power 53, which is why counts belong in ints.

Key idea

Floats are rounded to the nearest value they can hold, so compare them with is_equal_approx(), and remember that Vector2 and Vector3 keep only 32-bit precision.

Checkpoint

Check: Comparing floats

  1. One question A coin moves right by speed * delta every frame and should stop at x = 300. Which test stops it reliably?

    Choose one answer.

    Show the answer: One question

    Answer: if position.x >= 300.0:

    Moving floats rarely land on an exact value. Test a range (>= for a coin moving right), or is_equal_approx() when you want "close enough".

Practise: the damage of one hit

Fill in final_damage(), the one function of the starter file damage.gd. Its comment holds three rules: take the armour off, apply the multiplier with rounding, and never deal less than 1. maxi(a, b), one of the global functions like roundi(), returns the larger of two ints.

  1. Give the task a project of its own: a fresh folder holding an empty project.godot, damage.gd and check_damage.gd.
  2. Write the body of final_damage() in place of its return base line.
  3. From that folder, start the checker with godot --headless --no-header --script check_damage.gd.
damage.gd: the starter file GD · practice/damage.gd
extends Node

# Your task: the damage one hit deals.
#   1. Armour is taken off the base damage; negative armour (a weakness) adds to it.
#   2. The result is multiplied by the hit's multiplier (1.5 for a critical hit, 0.5 for a glancing blow) and
#      rounded to the nearest whole number, halves away from zero.
#   3. A hit always deals at least 1.
func final_damage(base: int, armor: int, multiplier: float) -> int:
	return base  # replace this line
check_damage.gd: the checker of this task GD · practice/check_damage.gd
extends SceneTree

# Checks damage.gd against the three rules. Run it in the project's folder:
# godot --headless --no-header --script check_damage.gd
const CASES := [
	[25, 8, 1.0, 17],
	[25, 8, 1.5, 26],
	[11, 0, 1.5, 17],
	[3, 2, 1.5, 2],
	[41, 10, 0.5, 16],
	[10, -4, 1.0, 14],
	[5, 9, 1.0, 1],
	[5, 9, 1.5, 1],
	[1_000_000_000, -1, 1.5, 1_500_000_002],
]


func _initialize() -> void:
	var damage: Node = load("res://damage.gd").new()
	var passed := 0
	for case in CASES:
		var got: int = damage.final_damage(case[0], case[1], case[2])
		var ok: bool = got == case[3]
		passed += int(ok)
		print("%s final_damage(%d, %d, %.1f) = %d (want %d)" % ["PASS" if ok else "FAIL", case[0], case[1], case[2], got, case[3]])
	damage.free()
	print("%d of %d checks passed" % [passed, CASES.size()])
	quit(0 if passed == CASES.size() else 1)

Here is the checker’s verdict on a correct damage.gd, and on one that hands the float back unrounded:

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

# The checker on a damage.gd that solves the task, then on one that returns the float without rounding it.
files("practice/project.godot=right/project.godot", "practice/check_damage.gd=right/check_damage.gd",
      "practice/solution/damage.gd=right/damage.gd",
      "practice/project.godot=truncated/project.godot", "practice/check_damage.gd=truncated/check_damage.gd",
      "practice/truncated/damage.gd=truncated/damage.gd")
run("""
cd right
godot --headless --no-header --script check_damage.gd
cd ../truncated
!godot --headless --no-header --script check_damage.gd
""")

Output

$ cd right
$ godot --headless --no-header --script check_damage.gd
PASS final_damage(25, 8, 1.0) = 17 (want 17)
PASS final_damage(25, 8, 1.5) = 26 (want 26)
PASS final_damage(11, 0, 1.5) = 17 (want 17)
PASS final_damage(3, 2, 1.5) = 2 (want 2)
PASS final_damage(41, 10, 0.5) = 16 (want 16)
PASS final_damage(10, -4, 1.0) = 14 (want 14)
PASS final_damage(5, 9, 1.0) = 1 (want 1)
PASS final_damage(5, 9, 1.5) = 1 (want 1)
PASS final_damage(1000000000, -1, 1.5) = 1500000002 (want 1500000002)
9 of 9 checks passed
$ cd ../truncated
$ godot --headless --no-header --script check_damage.gd
PASS final_damage(25, 8, 1.0) = 17 (want 17)
FAIL final_damage(25, 8, 1.5) = 25 (want 26)
FAIL final_damage(11, 0, 1.5) = 16 (want 17)
FAIL final_damage(3, 2, 1.5) = 1 (want 2)
FAIL final_damage(41, 10, 0.5) = 15 (want 16)
PASS final_damage(10, -4, 1.0) = 14 (want 14)
PASS final_damage(5, 9, 1.0) = 1 (want 1)
PASS final_damage(5, 9, 1.5) = 1 (want 1)
FAIL final_damage(1000000000, -1, 1.5) = 1500000001 (want 1500000002)
4 of 9 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 truncated answer fails exactly on the hits with a half: 25.5, 16.5, 1.5, 15.5 and the billion-point hit. Returning a float from a function declared -> int converted it with the fraction cut off, the same loss as int(). Your output should end with 9 of 9 checks passed. Then try the questions on the five ideas above: each gives its reason as soon as you pick an answer, and the ones you miss come back at the end:

Practice that adapts to you

Practise this lesson

12 questions about 5 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 var steps := 4 is followed by steps = 4.5 and print(steps). What is printed?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: 4

    The literal 4 is an int, so := makes steps an int for good. Write 4.0 when the variable must hold fractions.

  2. Question 2 of 12 What does Godot do with the line MAX_HP = 120 when MAX_HP is declared with const?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: Refuses it with a parse error, so the script does not load

    Constants cannot change while the game runs, and Godot checks that when it reads the script.

  3. Question 3 of 12 What does -9 / 2 give in GDScript?

    Type a number.

    Show the answer: Question 3 of 12

    Answer: -4

    Both sides are ints, so the fraction of -4.5 is dropped towards zero: -4, as -7 / 2 gave -3 in the run.

  4. Question 4 of 12 A row of 8 inventory slots wraps round. The cursor is on slot 0 and moves one to the left. What does posmod(0 - 1, 8) give?

    Type a number.

    Show the answer: Question 4 of 12

    Answer: 7

    posmod() wraps the same way in both directions, so -1 becomes 7, the last slot. (0 - 1) % 8 would give -1, a slot that does not exist.

  5. Question 5 of 12 What does 2 3 2 give in GDScript?

    Type a number.

    A hint

    The documentation says ** works from the left.

    Show the answer: Question 5 of 12

    Answer: 64

    is left-associative in GDScript, so this is (2 3) 2 = 8 2 = 64, not 2 ** 9 = 512 as in maths. Use brackets to say which you mean.

  6. Question 6 of 12 What int does the literal 0xFF stand for?

    Type a number.

    Show the answer: Question 6 of 12

    Answer: 255

    0x starts a hexadecimal literal: F is 15, so 0xFF is 15 × 16 + 15 = 255, as 0x1F was 31 in the run.

  7. Question 7 of 12 gold holds 9223372036854775807, the largest int, and the player picks up 1 more. What does gold hold?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: -9223372036854775808, the most negative int

    int is a 64-bit signed integer, and the int reference says it wraps around when it passes its bounds. A game has to cap such values itself.

  8. Question 8 of 12 What does int(-2.7) give?

    Type a number.

    Show the answer: Question 8 of 12

    Answer: -2

    int() cuts the fraction off towards zero, so -2.7 becomes -2, as int(-3.9) became -3. floori(-2.7) would give -3.

  9. Question 9 of 12 What does roundi(-0.5) give?

    Type a number.

    Show the answer: Question 9 of 12

    Answer: -1

    roundi() rounds halves away from zero, as roundi(-2.5) gave -3 in the run: -0.5 goes to -1.

  10. Question 10 of 12 score is an int holding 1250. Which line makes hud_text the String "Score: 1250"?

    Read the code, then choose one answer.

    var score := 1250
    var hud_text := ""
    # which line?
    Show the answer: Question 10 of 12

    Answer: hud_text = "Score: " + str(score)

    Text and numbers do not mix with +. Convert the number with str(), or use a format string, as the next lesson shows.

  11. Question 11 of 12 What does print(0.1 + 0.2 == 0.3) print?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: false

    0.1 and 0.2 are each stored as the nearest float that fits, and their sum lands just above 0.3. Use is_equal_approx(0.1 + 0.2, 0.3), which is true.

  12. Question 12 of 12 A 2D world places a town at x = 16777217 pixels. What x does Vector2(16777217, 0).x hold?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: 16777216.0, because a Vector2 keeps 32-bit floats

    Vector2 and Vector3 store 32-bit floats. Far from the origin, positions are rounded to steps of a pixel or more, which shows as shaking or jerky movement.

Interview questions

Warm-up: what is the difference between var hp = 100, var hp: int = 100 and var hp := 100?

var hp = 100 declares a variable with no static type: it holds an int now, but any later line may store a String or anything else in it, and nothing checks. var hp: int = 100 gives it the static type int, so Godot refuses a line that would store another type with a parse error before the script runs, and the editor’s completion knows what hp is. var hp := 100 is the same promise as the second, with the type inferred from the value: 100 is an int, so hp is an int for good. Inference needs a value with a known type; for := 7.0 it would be a float, which matters, because a float typed by accident stays a float. A constant, const MAX_HP := 100, can never be changed at all.

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

  1. Fresher to mid

    A design sheet says a critical hit deals 1.5 times the damage left after armour. Testers report that 11 damage crits for 16 instead of 17, and that crits against heavy armour heal the enemy. The code is crit = (base - armor) * 3 / 2. Find both bugs, fix them, and say which rounding you chose and why.

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

  2. Mid to senior

    A 2D exploration game puts its last region 20 million pixels from the origin. There, the player's sprite shakes while standing still and slow walking looks jerky; near the start everything is smooth. Explain the cause with numbers, and compare the fixes you would consider.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An untyped var can hold anything; var x: int and var x := 5 fix the type, so a wrong assignment becomes a parse error, and const values never change.
  • An int divided by an int drops the fraction towards zero, % takes the sign of the number divided, posmod() and fposmod() wrap instead, and ** works from the left.
  • An int is 64 bits and wraps around silently past its limit, and its bits can hold yes-or-no flags read with & and set with |.
  • int() cuts towards zero and roundi() rounds halves away from zero; "gems".to_int() is 0, so check text with is_valid_int(), and join text and numbers with str().
  • Floats are rounded to the nearest value they can hold: compare them with is_equal_approx(), and remember that Vector2 keeps only 32-bit precision.

Terms in this lesson

integer division
Dividing an int by an int, which gives an int with the fraction dropped towards zero: 7 / 2 is 3 and -7 / 2 is -3. With a float on either side, such as 7 / 2.0, the result is 3.5.
single-precision float
A 32-bit floating-point number with about 6 reliable digits, used inside Vector2, Vector3 and PackedFloat32Array. It holds every whole number only up to 16,777,216; a float variable is 64-bit.
static type
A type fixed for a variable when it is declared, as in var hp: int or var hp := 100. Godot then refuses any line that would store another type, with a parse error before the script runs.
type inference
Letting Godot take a variable's type from its first value, written :=. var speed := 4.5 makes speed a float for good, and var steps := 4 makes steps an int.

References

Related tools

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.