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.
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:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- loot changes kind. loot = "three gems" is allowed, because loot has no type. The run printed that loot now holds a String.
- 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.
- The constant refuses. MAX_HP = 120 is refused the same way: Cannot assign a new value to a constant.
- 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:
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() extends SceneTree
const MAX_HP := 100
func _initialize() -> void:
var hp: int = 75
hp = "full"
MAX_HP = 120
quit() 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:
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() 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() 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:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- An int to start. Armour 8 takes 25 down to 17. Both are ints, so left is an int.
- Times 3. left * 3 is 51. An int times an int is an int.
- 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.
- A float instead. left * 1.5 mixes an int with a float, and the result is a float: 25.5. Nothing is lost yet.
- int() cuts. int(25.5) turns it back into an int by cutting off the fraction: 25 again, the same as integer division.
- 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.
- 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
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.
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() 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():
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() extends SceneTree
func _initialize() -> void:
var score := 1250
print("Score: " + score)
quit() 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() 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
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:
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() 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
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.
- Give the task a project of its own: a fresh folder holding an empty
project.godot,damage.gdandcheck_damage.gd. - Write the body of
final_damage()in place of itsreturn baseline. - From that folder, start the checker with
godot --headless --no-header --script check_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 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:
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.
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.
-
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.
-
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.
Key takeaways
- An untyped
varcan hold anything;var x: intandvar x := 5fix the type, so a wrong assignment becomes a parse error, andconstvalues never change. - An int divided by an int drops the fraction towards zero,
%takes the sign of the number divided,posmod()andfposmod()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 androundi()rounds halves away from zero;"gems".to_int()is 0, so check text withis_valid_int(), and join text and numbers withstr().- 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
- GDScript reference (Godot Engine)
- Static typing in GDScript (Godot Engine)
- int class reference (Godot Engine)
- float class reference (Godot Engine)
- @GlobalScope class reference: posmod, fposmod, fmod, roundi and is_equal_approx (Godot Engine)
- String class reference: to_int and is_valid_int (Godot Engine)
- GDScript warning system (Godot Engine)
- ProjectSettings class reference: debug/gdscript/warnings (Godot Engine)
- GDScript style guide: naming conventions (Godot Engine)
- Large world coordinates (Godot Engine)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress