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 6 – Game math and 2D physics

Rigid bodies, raycasts and physics queries

Push Godot 4.7 RigidBody2D crates with impulses and forces, ask the world with rays, shape casts and space queries, and learn when physics results repeat.

  • Intermediate
  • 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

  • Apply impulses and forces to RigidBody2D objects, and let them sleep
  • Query the world with RayCast2D, ShapeCast2D and direct space queries
  • Filter queries with exclusions, collision masks and their other options
  • Decide which physics results a test or a replay can rely on

Before you start

What you’ll learn · Before you start

What you will learn

  • Apply impulses and forces to RigidBody2D objects, and let them sleep
  • Query the world with RayCast2D, ShapeCast2D and direct space queries
  • Filter queries with exclusions, collision masks and their other options
  • Decide which physics results a test or a replay can rely on

Before you start

On this page

The same kick sent a 1 kg crate 76.1 px across the floor and a 4 kg crate only 4.2 px: four times the mass gave a quarter of the speed and about an eighteenth of the distance. Rigid bodies are moved by the physics engine, so a script steers them by forces and impulses and lets the solver do the rest.

The other half of physics in a game is asking questions: can the guard see the player, how far below is the ground, will this box fit through that door, what does the blast reach. This page pushes crates and watches them sleep, asks the world with rays, shape casts and direct space queries and filters what they may hit, and runs one collision several times to see which changes alter the result. The speeds, hits and resting places quoted here are what Godot 4.7.2 printed, running each script without a window.

An impulse changes the speed at once, a force a little each tick

A RigidBody2D cannot be moved directly: its reference says “you must apply forces to it”, and warns that changing its transform or linear_velocity “very often may lead to some unpredictable behaviors”. Two kinds of push are left. An impulse: A push that changes a body's speed at once, by the impulse divided by the mass, whatever the tick's length. apply_central_impulse(Vector2(400, 0)) gave a 1 kg crate 400 px a second and a 4 kg crate 100. changes the speed at once, by the impulse divided by the mass, and the reference calls it “time-independent”. A force changes the speed a little on every tick it is applied, and is “meant to be applied every physics update”. This run drops four crates, lets them settle, then kicks three of them with the same impulse and pushes the fourth with a force for half a second:

crate_push.gd: one impulse, three masses, and a force GD · crate_push.gd
extends SceneTree

# Three crates of 1, 2 and 4 kg drop onto a floor, each in a lane of its own, and fall asleep. On tick 90 each gets
# the same impulse, 400 to the right with apply_central_impulse(). The run reports each crate's speed one tick later,
# where friction stops it and when it falls asleep again. A fourth crate of 1 kg gets a force of 1,500 to the right
# from apply_central_force() on every tick from 90 to 119, half a second.
# Run it with --fixed-fps 60, so that every frame has exactly one physics tick.

var tick := 0
var crates: Array[RigidBody2D] = []
var start_x := {}
var slept := {}


func place(node: CollisionObject2D, size: Vector2, at: Vector2) -> void:
	var shape := CollisionShape2D.new()
	var rect := RectangleShape2D.new()
	rect.size = size
	shape.shape = rect
	node.add_child(shape)
	node.position = at
	root.add_child(node)


func _initialize() -> void:
	print("Sleep: after %.1f s below %.1f px a second; gravity %d px/s²" % [
		ProjectSettings.get_setting("physics/2d/time_before_sleep"),
		ProjectSettings.get_setting("physics/2d/sleep_threshold_linear"),
		ProjectSettings.get_setting("physics/2d/default_gravity")])
	for i in 4:
		var y := 1000.0 * i
		place(StaticBody2D.new(), Vector2(4000, 40), Vector2(1000, y + 320))
		var crate := RigidBody2D.new()
		crate.name = ["Crate 1 kg", "Crate 2 kg", "Crate 4 kg", "Pushed 1 kg"][i]
		crate.mass = [1.0, 2.0, 4.0, 1.0][i]
		place(crate, Vector2(40, 40), Vector2(100, y + 120))
		crate.sleeping_state_changed.connect(func() -> void:
			if crate.sleeping:
				slept[crate.name] = tick)
		crates.append(crate)


func _physics_process(_delta: float) -> bool:
	tick += 1
	if tick == 89:
		for crate in crates:
			print("tick 89: %s at x %.1f, asleep since tick %d" % [crate.name, crate.position.x, slept[crate.name]])
			start_x[crate.name] = crate.position.x
			slept.erase(crate.name)
	if tick == 90:
		for crate in crates.slice(0, 3):
			crate.apply_central_impulse(Vector2(400, 0))
	if tick >= 90 and tick < 120:
		crates[3].apply_central_force(Vector2(1500, 0))
	if tick == 91:
		for crate in crates:
			print("tick 91: %s moves at %.1f px a second" % [crate.name, crate.linear_velocity.x])
	if tick == 120:
		print("tick 120: %s moves at %.1f px a second, after 30 ticks of force" % [crates[3].name,
			crates[3].linear_velocity.x])
	if tick < 300:
		return false
	for crate in crates:
		print("%s: travelled %.1f px, asleep again on tick %s" % [crate.name, crate.position.x - start_x[crate.name],
			slept.get(crate.name, "-")])
	return true
Impulses and a force on resting crates, Godot 4.7.2 Python · crate_session.py
from session import files, run

files("project.godot", "crate_push.gd")
run("""
godot --headless --no-header --fixed-fps 60 --script crate_push.gd
""")

Output

$ godot --headless --no-header --fixed-fps 60 --script crate_push.gd
Sleep: after 0.5 s below 2.0 px a second; gravity 980 px/s²
tick 89: Crate 1 kg at x 100.0, asleep since tick 65
tick 89: Crate 2 kg at x 100.0, asleep since tick 65
tick 89: Crate 4 kg at x 100.0, asleep since tick 65
tick 89: Pushed 1 kg at x 100.0, asleep since tick 65
tick 91: Crate 1 kg moves at 383.0 px a second
tick 91: Crate 2 kg moves at 183.3 px a second
tick 91: Crate 4 kg moves at 83.5 px a second
tick 91: Pushed 1 kg moves at 8.7 px a second
tick 120: Pushed 1 kg moves at 296.6 px a second, after 30 ticks of force
Crate 1 kg: travelled 76.1 px, asleep again on tick 142
Crate 2 kg: travelled 18.5 px, asleep again on tick 130
Crate 4 kg: travelled 4.2 px, asleep again on tick 124
Pushed 1 kg: travelled 127.7 px, asleep again on tick 192

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

The impulse of 400 gave the crates 400, 200 and 100 px a second, less one tick of friction: 383.0, 183.3 and 83.5 one tick later. Friction then took about 16.7 px a second off every crate each tick, whatever its mass, so the distance went with the square of the speed: 76.1 px for 1 kg, 18.5 for 2 kg and 4.2 for 4 kg. The force of 1,500 on the fourth crate had to beat that same friction before it gained any speed; after 30 ticks it moved at 296.6 px a second and slid 127.7 px in all.

Crate Speed one tick after the push Distance Asleep again on tick
1 kg, impulse 400 383.0 px/s 76.1 px 142
2 kg, impulse 400 183.3 px/s 18.5 px 130
4 kg, impulse 400 83.5 px/s 4.2 px 124
1 kg, force 1,500 for 0.5 s 8.7 px/s 127.7 px 192

All four had fallen asleep on tick 65, and woke when pushed. A sleeping body: A RigidBody2D that has rested for time_before_sleep, 0.5 s by default, and that the engine stops simulating until a collision, an impulse or code wakes it. It then acts like a static body. is one the engine has stopped simulating: the Physics introduction says a rigid body at rest long enough “goes to sleep” and acts like a static body until something wakes it. The run’s project settings give the rule’s numbers: asleep after 0.5 s below 2 px a second. A crate that must keep moving under its own forces sets can_sleep = false.

Steering a rigid body by its position

crate.position = target on every tick teleports the body past the solver, which then fights it. Push with apply_central_impulse() for a kick, apply_central_force() for a steady push, or change its motion inside _integrate_forces(), which the reference says lets you “read and safely modify the simulation state”.

Force Calculator (Newton’s Second Law) Check force, mass and acceleration: 1,500 on 1 kg is 1,500 px/s² before friction.

Checkpoint

Check: Impulses, forces and sleeping bodies

  1. Question 1 of 2 A resting crate of 5 kg gets apply_central_impulse(Vector2(400, 0)). What speed does the impulse give it, before friction?

    Type a number.

    A hint

    An impulse changes the speed by the impulse divided by the mass.

    Show the answer: Question 1 of 2

    Answer: 80 px/s

    400 / 5 = 80 px a second. In the run the 4 kg crate got 100 and showed 83.5 one tick later, after one tick of friction.

  2. Question 2 of 2 A jet pack should push a RigidBody2D hero up for as long as a button is held. Which call, and how often?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: apply_central_force() on every physics tick while the button is held

    Impulses are kicks (a jump, a hit, a blast); forces are pushes that last (thrust, wind).

A ray answers what lies along a line

A guard’s sight, a gun’s aim and a hero’s feet all ask one question: what is the first thing on this line? The Ray-casting page gets the answer from the world’s direct space state, get_world_2d().direct_space_state, and warns that “the only time accessing space is safe is during the Node._physics_process() callback”. A space query: A question asked of the physics world through PhysicsDirectSpaceState2D: what a ray hits, what a shape overlaps, how far a shape can move. It answers at once, and the docs say to ask it in _physics_process(). answers at once, as a dictionary: empty when nothing was hit, otherwise the collider, the position of the hit and the normal of the surface there. This enemy looks at a player through a world with one wall, first with the player behind it and then above it, and a RayCast2D under the player measures the ground:

line_of_sight.gd: an enemy's sight line and a ground probe GD · line_of_sight.gd
extends SceneTree

# An enemy at (100, 200) looks at a player through a world with one wall, 20 px wide and 100 px tall, at x = 300.
# Layers: 1 world (the wall and the floor), 2 player, 3 enemies. Every query runs in _physics_process(), where the
# docs say the space may be read. Then a RayCast2D under the player measures the ground, before and after the player
# moves.

var tick := 0
var enemy := CharacterBody2D.new()
var player := CharacterBody2D.new()
var probe := RayCast2D.new()


func place(node: CollisionObject2D, size: Vector2, at: Vector2, layer: int) -> void:
	var shape := CollisionShape2D.new()
	var rect := RectangleShape2D.new()
	rect.size = size
	shape.shape = rect
	node.add_child(shape)
	node.position = at
	node.collision_layer = 1 << (layer - 1)
	root.add_child(node)


func ray(from: Vector2, to: Vector2, mask: int) -> Dictionary:
	var query := PhysicsRayQueryParameters2D.create(from, to, mask, [enemy.get_rid()])
	return root.world_2d.direct_space_state.intersect_ray(query)


func describe(hit: Dictionary) -> String:
	if hit.is_empty():
		return "nothing in the way"
	return "hit %s at %s, normal %s" % [hit.collider.name, hit.position, hit.normal]


func _initialize() -> void:
	var wall := StaticBody2D.new()
	wall.name = "Wall"
	place(wall, Vector2(20, 100), Vector2(300, 220), 1)
	var floor_body := StaticBody2D.new()
	floor_body.name = "Floor"
	place(floor_body, Vector2(1000, 40), Vector2(500, 450), 1)
	enemy.name = "Enemy"
	place(enemy, Vector2(30, 30), Vector2(100, 200), 3)
	player.name = "Player"
	place(player, Vector2(30, 30), Vector2(500, 200), 2)
	probe.target_position = Vector2(0, 400) # 400 px straight down, relative to the probe
	player.add_child(probe) # exclude_parent is true, so the probe never reports the player


func _physics_process(_delta: float) -> bool:
	tick += 1
	if tick == 2:
		print("Tick 2, player at %s, behind the wall:" % player.position)
		print("  mask world + player: %s" % describe(ray(enemy.position, player.position, 0b011)))
		print("  mask world only:     %s" % describe(ray(enemy.position, player.position, 0b001)))
		player.position = Vector2(500, 120)
	elif tick == 3:
		print("Tick 3, player at %s, above the wall's top:" % player.position)
		print("  mask world + player: %s" % describe(ray(enemy.position, player.position, 0b011)))
		print("  mask world only:     %s" % describe(ray(enemy.position, player.position, 0b001)))
		print("  ground probe: colliding %s at %s, %.1f px below the player" % [probe.is_colliding(),
			probe.get_collision_point(), probe.get_collision_point().y - player.position.y])
		player.position = Vector2(1200, 120) # past the floor's end at x = 1000
		print("Player moved to %s, past the floor's end:" % player.position)
		print("  ground probe, same tick:          colliding %s" % probe.is_colliding())
		probe.force_raycast_update()
		print("  after force_raycast_update():     colliding %s" % probe.is_colliding())
		return true
	return false
Two ticks of sight lines and a probe, Godot 4.7.2 Python · los_session.py
from session import files, run

files("project.godot", "line_of_sight.gd")
run("""
godot --headless --no-header --fixed-fps 60 --script line_of_sight.gd
""")

Output

$ godot --headless --no-header --fixed-fps 60 --script line_of_sight.gd
Tick 2, player at (500.0, 200.0), behind the wall:
  mask world + player: hit Wall at (290.0, 200.0), normal (-1.0, 0.0)
  mask world only:     hit Wall at (290.0, 200.0), normal (-1.0, 0.0)
Tick 3, player at (500.0, 120.0), above the wall's top:
  mask world + player: hit Player at (485.0, 123.0), normal (-1.0, 0.0)
  mask world only:     nothing in the way
  ground probe: colliding true at (500.0, 430.0), 310.0 px below the player
Player moved to (1200.0, 120.0), past the floor's end:
  ground probe, same tick:          colliding true
  after force_raycast_update():     colliding false

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

Behind the wall, both rays stopped at its face, (290, 200), with a normal of (-1, 0), pointing back at the enemy. Above it, the two masks gave two answers that mean the same thing: with the player’s layer included the ray reported the player itself at (485, 123), the edge of its shape, and with the world’s layer alone it reported nothing in the way. Step through both ticks:

Interactive · step by step An enemy's line of sight and a probe under the player Step through the run's ticks. The sub-labels hold what each query returned.
An enemy's line of sight and a probe under the player, step 5 of 5. Showing Enemy's eye, Player (at (500, 120), layer 2), Wall, The ray's answer (empty: nothing in the way), RayCast2D probe (false after the update), Floor. In focus: RayCast2D probe.Enemy's eyeat (100, 200)Playerat (500, 120), layer 2Wallx 300, y 170 to 270The ray's answerempty: nothing in the wayRayCast2D probefalse after the updateFloortop at y 430, ends x 1000

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

Every step as text
  1. Behind the wall. On tick 2 the player stands at (500, 200), behind the wall. The ray from the enemy's eye stops at the wall's face, (290, 200), whichever of the two masks it uses.
  2. Above the wall. On tick 3 the player is at (500, 120), above the wall's top. With the player's layer in the mask, the ray reports the player at (485, 123), the edge of its shape.
  3. The world only. With the world's layer alone in the mask, the same ray reports nothing, an empty dictionary. Both answers mean the player is visible, read in two ways.
  4. The ground probe. A RayCast2D child of the player, 400 px long and pointing down, reports the floor at (500, 430), 310 px below the player. It never reports the player, its parent.
  5. A stale answer. The player is moved to (1200, 120), past the floor's end. In the same tick the probe still says it is colliding; after force_raycast_update() it says false.

The probe is a RayCast2D, a node that casts its ray on every physics frame and, its reference says, “holds the result until the next physics frame”. Its target_position is relative to the node, here 400 px straight down, and exclude_parent, true by default, kept it from reporting the player it belongs to. The last lines show the catch: after the player was moved past the floor’s end, the probe still said it was colliding until force_raycast_update() cast it again.

Key idea

A sight line is a ray whose mask holds only what can block sight: an empty answer means the target is visible, and any hit names what is in the way.

Checkpoint

Check: Ray queries and RayCast2D

  1. Question 1 of 2 A guard's sight check casts a ray from its eye to the player with a mask of the world's layer only. What does an empty result mean?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing that blocks sight lies between them, so the guard sees the player

    With the player's layer in the mask too, a clear line reports the player itself as the first hit; both designs work if the code reads them the right way.

  2. Question 2 of 2 A script moves a hero and, in the same tick, reads its RayCast2D child's is_colliding(). What does it get?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The result from the last physics frame, until force_raycast_update() casts it again

    Call force_raycast_update() when the ray or its parent changed and the answer is needed in the same frame.

Exclusions and masks decide what a query may hit

PhysicsRayQueryParameters2D.create(from, to) starts with a mask of every layer, no exclusions, and two options off: hit_from_inside, for rays that start inside a shape, and collide_with_areas, for Area2D shapes. This run asks one ray five ways, from the enemy’s centre to the player’s, past a trigger area and a wall:

query_filters.gd: one ray asked five ways GD · query_filters.gd
extends SceneTree

# One ray, from the enemy's centre at (100, 200) to the player's at (500, 200), asked five ways. On the way lie a
# trigger Area2D at x = 200 and a wall at x = 300. Layers: 1 world, 2 player, 3 enemies, 4 triggers.

var tick := 0
var enemy := CharacterBody2D.new()


func place(node: CollisionObject2D, size: Vector2, at: Vector2, layer: int) -> void:
	var shape := CollisionShape2D.new()
	var rect := RectangleShape2D.new()
	rect.size = size
	shape.shape = rect
	node.add_child(shape)
	node.position = at
	node.collision_layer = 1 << (layer - 1)
	root.add_child(node)


func ask(label: String, query: PhysicsRayQueryParameters2D) -> void:
	var hit := root.world_2d.direct_space_state.intersect_ray(query)
	if hit.is_empty():
		print("%-40s nothing" % label)
	else:
		print("%-40s %s at %s, normal %s" % [label, hit.collider.name, hit.position, hit.normal])


func _initialize() -> void:
	enemy.name = "Enemy"
	place(enemy, Vector2(30, 30), Vector2(100, 200), 3)
	var trigger := Area2D.new()
	trigger.name = "Trigger"
	place(trigger, Vector2(20, 100), Vector2(200, 200), 4)
	var wall := StaticBody2D.new()
	wall.name = "Wall"
	place(wall, Vector2(20, 100), Vector2(300, 200), 1)
	var player := CharacterBody2D.new()
	player.name = "Player"
	place(player, Vector2(30, 30), Vector2(500, 200), 2)


func _physics_process(_delta: float) -> bool:
	tick += 1
	if tick < 2:
		return false
	var from := Vector2(100, 200)
	var to := Vector2(500, 200)
	var q := PhysicsRayQueryParameters2D.create(from, to)
	print("create() defaults: mask %d, exclude %s, hit_from_inside %s, collide_with_areas %s" % [q.collision_mask,
		q.exclude, q.hit_from_inside, q.collide_with_areas])
	ask("1. the defaults", q)
	q = PhysicsRayQueryParameters2D.create(from, to)
	q.hit_from_inside = true
	ask("2. hit_from_inside", q)
	q = PhysicsRayQueryParameters2D.create(from, to, 0xFFFFFFFF, [enemy.get_rid()])
	q.hit_from_inside = true
	ask("3. hit_from_inside, the enemy excluded", q)
	q = PhysicsRayQueryParameters2D.create(from, to, 0xFFFFFFFF, [enemy.get_rid()])
	q.collide_with_areas = true
	ask("4. collide_with_areas", q)
	ask("5. mask: the player's layer only", PhysicsRayQueryParameters2D.create(from, to, 0b0010))
	return true
The same ray with five settings, Godot 4.7.2 Python · filters_session.py
from session import files, run

files("project.godot", "query_filters.gd")
run("""
godot --headless --no-header --fixed-fps 60 --script query_filters.gd
""")

Output

$ godot --headless --no-header --fixed-fps 60 --script query_filters.gd
create() defaults: mask 4294967295, exclude [], hit_from_inside false, collide_with_areas false
1. the defaults                          Wall at (290.0, 200.0), normal (-1.0, 0.0)
2. hit_from_inside                       Enemy at (100.0, 200.0), normal (0.0, 0.0)
3. hit_from_inside, the enemy excluded   Wall at (290.0, 200.0), normal (-1.0, 0.0)
4. collide_with_areas                    Trigger at (190.0, 200.0), normal (-1.0, 0.0)
5. mask: the player's layer only         Player at (485.0, 200.0), normal (-1.0, 0.0)

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

With the defaults the ray skipped the enemy it started inside and the trigger, which is an area, and stopped at the wall. With hit_from_inside on it reported the enemy itself, at the ray’s start, with a normal of (0, 0), which the reference documents for hits from inside; excluding the enemy’s RID put the wall back. collide_with_areas made the trigger the first hit, at (190, 200), and a mask of the player’s layer alone let the ray pass the wall and reach the player.

The Ray-casting page compares the two ways to leave things out: an exclude list of RIDs works for one body, such as the caster itself, but for whole groups “it is much more efficient to use the collision layer/mask system”. So a query’s mask names the layers that matter, and exclude holds the few bodies that are on those layers but must not count.

A sight line with the default mask

create(eye, target) looks at every layer, so the ray reports the target’s own body, a crate or a bullet in the way, and a sight check reads that as blocked. Give the query the blockers’ layers, as the practice’s tempting version failed to do.

Checkpoint

Check: Masks, exclusions and query options

  1. Question 1 of 2 A ray starts at the centre of the enemy that casts it. With the default settings, does it report the enemy?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No; hit_from_inside is false by default, so a ray starting inside a shape skips it

    Excluding the caster's RID is still the safe habit: a muzzle point outside the body would hit it otherwise.

  2. Question 2 of 2 A ray must report a trigger Area2D that lies on the way. Which option does it need?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: collide_with_areas = true, with the trigger's layer in the mask

    collide_with_bodies and collide_with_areas choose the kinds of object; the mask chooses their layers.

A shape cast asks whether a whole body fits

A ray is a line, so it answers for something with no width. A box of 40 px that should go through a doorway of 30 px needs a shape cast: A query that sweeps a whole shape along a motion and reports where it would first touch something, as a fraction of the motion. Unlike a ray, it knows whether a body of that size fits., which sweeps the whole shape along the motion. ShapeCast2D does it as a node, and the space state’s cast_motion() and intersect_shape() do it as direct queries. This run sweeps a ray, a 40 px box and a 20 px box towards a doorway, then asks which crates a blast circle reaches:

shape_queries.gd: a doorway and a blast GD · shape_queries.gd
extends SceneTree

# Will it fit? A doorway in a wall at x = 300 leaves a gap of 30 px, from y = 185 to y = 215. A ray, a box of 40 px and
# a box of 20 px are each swept from (100, 200) towards the doorway, 400 px to the right. Then a circle of radius 150
# asks which of three crates lie in a blast at (800, 200).

var tick := 0
var big := ShapeCast2D.new()
var small := ShapeCast2D.new()


func block(name: String, size: Vector2, at: Vector2, body: CollisionObject2D = StaticBody2D.new()) -> void:
	var shape := CollisionShape2D.new()
	var rect := RectangleShape2D.new()
	rect.size = size
	shape.shape = rect
	body.add_child(shape)
	body.name = name
	body.position = at
	root.add_child(body)


func caster(cast: ShapeCast2D, side: float) -> void:
	var box := RectangleShape2D.new()
	box.size = Vector2(side, side)
	cast.shape = box
	cast.position = Vector2(100, 200)
	cast.target_position = Vector2(400, 0)
	root.add_child(cast)


func _initialize() -> void:
	block("WallTop", Vector2(20, 185), Vector2(300, 92.5))
	block("WallBottom", Vector2(20, 185), Vector2(300, 307.5))
	for i in 3:
		block("Crate%d" % (i + 1), Vector2(30, 30), Vector2(800 + [50, 120, 200][i], 200), RigidBody2D.new())
	caster(big, 40.0)
	caster(small, 20.0)


func _physics_process(_delta: float) -> bool:
	tick += 1
	if tick < 2:
		return false
	var space := root.world_2d.direct_space_state
	var hit := space.intersect_ray(PhysicsRayQueryParameters2D.create(Vector2(100, 200), Vector2(500, 200)))
	print("ray through the doorway's middle: %s" % ("nothing in the way" if hit.is_empty() else hit.collider.name))
	for cast: ShapeCast2D in [big, small]:
		cast.force_shapecast_update()
		var side: float = (cast.shape as RectangleShape2D).size.x
		if cast.is_colliding():
			print("box of %d px: hits %s, safe fraction %.3f, %.1f px of the 400" % [side, cast.get_collider(0).name,
				cast.get_closest_collision_safe_fraction(), cast.get_closest_collision_safe_fraction() * 400.0])
		else:
			print("box of %d px: passes, safe fraction %.3f" % [side, cast.get_closest_collision_safe_fraction()])
	var motion := PhysicsShapeQueryParameters2D.new()
	motion.shape = big.shape
	motion.transform = Transform2D(0.0, Vector2(100, 200))
	motion.motion = Vector2(400, 0)
	print("cast_motion() for the 40 px box: %s" % space.cast_motion(motion))
	var blast := PhysicsShapeQueryParameters2D.new()
	var circle := CircleShape2D.new()
	circle.radius = 150.0
	blast.shape = circle
	blast.transform = Transform2D(0.0, Vector2(800, 200))
	var names := space.intersect_shape(blast).map(func(r: Dictionary) -> String: return r.collider.name)
	names.sort()
	print("blast of radius 150 at (800, 200) finds: %s" % ", ".join(names))
	return true
A ray, two boxes and a circle, Godot 4.7.2 Python · shapes_session.py
from session import files, run

files("project.godot", "shape_queries.gd")
run("""
godot --headless --no-header --fixed-fps 60 --script shape_queries.gd
""")

Output

$ godot --headless --no-header --fixed-fps 60 --script shape_queries.gd
ray through the doorway's middle: nothing in the way
box of 40 px: hits WallBottom, safe fraction 0.422, 168.8 px of the 400
box of 20 px: passes, safe fraction 1.000
cast_motion() for the 40 px box: [0.421875, 0.42578125]
blast of radius 150 at (800, 200) finds: Crate1, Crate2

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

The ray went through the middle of the doorway and reported nothing, the 20 px box passed, and the 40 px box stopped at the wall after 168.8 px of its 400: a safe fraction of 0.422. The figure draws the three to scale:

A wall with a 30 px doorway: a ray passes through its middle, a 40 px box stops at the wall after 168.8 px, a 20 px box passes.Will it fit through a 30 px doorway?40 px box stopsafter 168.8 px20 px boxpasseswall at x 300start (100, 200)a ray: nothing in the waya 40 px box: safe fraction 0.422a 20 px box: safe fraction 1.000Each swept 400 px to the right. Gap: y 185 to 215.

A ray, a 40 px box and a 20 px box swept towards a 30 px doorway

Text description of the diagram

A wall stands at x = 300, 20 px thick, with a doorway that leaves a gap from y = 185 to y = 215, 30 px. Everything starts at (100, 200), level with the middle of the gap, and is swept 400 px to the right.

  1. A ray, the dashed line, passes through the middle of the doorway and reports nothing in the way, although only a line could fit there.
  2. A box of 40 px, drawn as an outline, stops with its front edge against the wall: it travels 168.8 px of the 400, a safe fraction of 0.422, and reports the lower part of the wall.
  3. A box of 20 px, drawn filled beyond the wall, passes through the gap: its safe fraction is 1.000, the whole motion.

cast_motion() answered with two fractions, as its reference describes, “the safe and unsafe proportions”: 0.421875, the furthest the box can move without touching, and 0.42578125, the first point where it would. The true contact, 170 px of 400, lies between them. intersect_shape() with a circle of radius 150 at the blast’s centre found the two crates whose shapes it overlapped, Crate1 and Crate2, and not the one 200 px away; the practice turns that list into pushes that fall off with distance.

Checkpoint

Check: Shape casts and shape queries

  1. Question 1 of 2 A ray through the middle of a 30 px doorway reports nothing in the way. Can a 40 px box get through?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Not necessarily; only a shape cast of the box can tell, and in the run it stopped at the wall

    Sweep the shape that will really move: ShapeCast2D, or cast_motion() with the body's own shape.

  2. Question 2 of 2 Which query lists every body that a circular blast of radius 150 touches?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: intersect_shape() with a CircleShape2D of radius 150 at the blast's centre

    intersect_shape() returns at most max_results colliders, 32 by default; pass a larger number for crowded blasts.

The same build, inputs and tick rate repeat a result; change one and it differs

A replay that stores only the player’s inputs, or a test that expects a crate to come to rest at a given x, needs the simulation to produce the same numbers again. This run pushes a chain of three crates and prints where they come to rest, four times: twice the same way, once at 120 ticks a second, and once with the crates added to the tree in the opposite order:

crate_chain.gd: a chain of three crates GD · crate_chain.gd
extends SceneTree

# A chain of three crates on a floor: 1 kg at x = 100, 2 kg at 160 and 4 kg at 230. After one second of game time the
# first crate gets an impulse of 900 to the right and runs into the others. After six seconds the run prints where
# each crate came to rest. The arguments after "--" set the physics tick rate and, with "reversed", add the crates to
# the tree in the opposite order.
# Run it with --fixed-fps 60: one frame of 1/60 s each, so the game time is the same at every tick rate.

var tick := 0
var crates: Array[RigidBody2D] = []
var pushed := false


func place(node: CollisionObject2D, size: Vector2, at: Vector2) -> void:
	var shape := CollisionShape2D.new()
	var rect := RectangleShape2D.new()
	rect.size = size
	shape.shape = rect
	node.add_child(shape)
	node.position = at
	root.add_child(node)


func _initialize() -> void:
	var args := OS.get_cmdline_user_args()
	Engine.physics_ticks_per_second = int(args[0])
	place(StaticBody2D.new(), Vector2(4000, 40), Vector2(1000, 320))
	var order := [0, 1, 2]
	if args.size() > 1 and args[1] == "reversed":
		order.reverse()
	for i: int in order:
		var crate := RigidBody2D.new()
		crate.name = "Crate%d" % i
		crate.mass = [1.0, 2.0, 4.0][i]
		place(crate, Vector2(40, 40), Vector2([100.0, 160.0, 230.0][i], 278))
		crates.append(crate)
	crates.sort_custom(func(a: RigidBody2D, b: RigidBody2D) -> bool: return String(a.name) < String(b.name))


func _physics_process(_delta: float) -> bool:
	tick += 1
	var time := float(tick) / Engine.physics_ticks_per_second
	if not pushed and time >= 1.0:
		pushed = true
		crates[0].apply_central_impulse(Vector2(900, 0))
	if time < 6.0:
		return false
	var rest: Array[String] = []
	for crate in crates:
		rest.append("x %.3f" % crate.position.x)
	print("%3d ticks a second%s: %s" % [Engine.physics_ticks_per_second,
		", reversed" if OS.get_cmdline_user_args().size() > 1 else "", ", ".join(rest)])
	return true
The same collision four times, Godot 4.7.2 Python · chain_session.py
from session import files, run

# The same chain four times: twice at 60 ticks a second, once at 120, once at 60 with the crates added the other way.
files("project.godot", "crate_chain.gd")
run("""
godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 60
godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 60
godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 120
godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 60 reversed
""")

Output

$ godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 60
 60 ticks a second: x 152.230, x 191.931, x 231.631
$ godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 60
 60 ticks a second: x 152.230, x 191.931, x 231.631
$ godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 120
120 ticks a second: x 152.723, x 192.428, x 232.128
$ godot --headless --no-header --fixed-fps 60 --script crate_chain.gd -- 60 reversed
 60 ticks a second, reversed: x 152.233, x 191.936, x 231.636

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

Run First crate at rest Second Third
60 ticks a second 152.230 191.931 231.631
60 ticks a second, again 152.230 191.931 231.631
120 ticks a second 152.723 192.428 232.128
60 ticks a second, crates added in reverse 152.233 191.936 231.636

The two identical runs agreed to the last digit printed. Doubling the tick rate moved every crate about half a pixel. A change that alters no number at all did it too: adding the same crates in the opposite order moved them by 3 to 5 thousandths of a pixel. The Jitter, stutter and input lag page warns about tick rates in the same terms: “different physics FPS will lead to different outcomes in physics simulation”, even with delta used throughout, so players should not be allowed to change it in competitive multiplayer games. A run on one machine with one build says nothing about another processor, operating system or Godot version.

Key idea

Rely on a physics result only where everything that fed it is the same: the build, the tick rate, the inputs and the order of events. Tests pin those with --fixed-fps and fixed scenes; networked games send the results, not only the inputs.

Checkpoint

Check: When physics results repeat

  1. Question 1 of 2 In the run, which change left the crates exactly where they came to rest before?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Running the same thing again with the same build and settings

    Same build, same inputs, same tick rate and same order of events repeated the result on this machine; each change of those changed it.

  2. Question 2 of 2 A racing game records only the player's inputs for replays, and on some machines the replay car ends in a wall. What is the sound fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Record the car's position and rotation every tick or so as well, and play them back

    Re-simulating inputs needs the same results everywhere; recording the results needs nothing but storage.

Practise: sight, ground and a blast with falloff

Write three helpers on top of the space state. queries.gd has three functions to finish: has_line_of_sight() tells whether anything on the blockers’ layers lies on a line, ground_distance() measures how far below a point the ground is, or -1.0 if there is none within a distance, and explode() pushes every rigid body within a radius away from a centre, with an impulse that falls from full strength at the centre to 0 at the radius.

  1. In a Godot 4.7 project, save queries.gd and check_queries.gd from below.
  2. Finish the three functions. They are called from _physics_process(), with the space as their first argument.
  3. In a terminal in the project’s folder, run godot --headless --no-header --fixed-fps 60 --script check_queries.gd.
queries.gd: the starter GD · practice/queries.gd
extends RefCounted

# Three helpers built on physics queries. Each takes the space to ask, from get_world_2d().direct_space_state, and is
# called from _physics_process(). Finish them; check_queries.gd checks them.


## Whether nothing on the layers of blockers_mask lies on the straight line from eye to target.
func has_line_of_sight(space: PhysicsDirectSpaceState2D, eye: Vector2, target: Vector2, blockers_mask: int) -> bool:
	return false


## How far below point the first ground on the layers of ground_mask is, or -1.0 if there is none within max_distance.
func ground_distance(space: PhysicsDirectSpaceState2D, point: Vector2, max_distance: float, ground_mask: int) -> float:
	return -1.0


## Pushes every RigidBody2D on the layers of mask whose origin lies within radius of center away from it, with an
## impulse of strength at the centre falling to 0 at the radius. Returns how many bodies it pushed.
func explode(space: PhysicsDirectSpaceState2D, center: Vector2, radius: float, strength: float, mask: int) -> int:
	return 0
check_queries.gd: the checker of this task GD · practice/check_queries.gd
extends SceneTree

# Checks queries.gd of the practice. Run it in the project's folder:
#   godot --headless --no-header --fixed-fps 60 --script check_queries.gd
# The world: a floor from x = 0 to 1000 whose top is y = 300 and a wall at x = 300 from y = 150 to 250, on layer 1;
# the player, 30 px square, at (500, 120) on layer 2; three crates on the floor at x = 700, 760 and 900 on layer 3.

const WORLD := 0b001
const CRATES := 0b100

var queries: RefCounted
var crates: Array[RigidBody2D] = []
var tick := 0
var passed := 0
var total := 0


func place(node: CollisionObject2D, size: Vector2, at: Vector2, layer: int) -> void:
	var shape := CollisionShape2D.new()
	var rect := RectangleShape2D.new()
	rect.size = size
	shape.shape = rect
	node.add_child(shape)
	node.position = at
	node.collision_layer = 1 << (layer - 1)
	root.add_child(node)


func _initialize() -> void:
	queries = load("res://queries.gd").new()
	place(StaticBody2D.new(), Vector2(1000, 40), Vector2(500, 320), 1)
	place(StaticBody2D.new(), Vector2(20, 100), Vector2(300, 200), 1)
	place(CharacterBody2D.new(), Vector2(30, 30), Vector2(500, 120), 2)
	for x: float in [700.0, 760.0, 900.0]:
		var crate := RigidBody2D.new()
		place(crate, Vector2(30, 30), Vector2(x, 285), 3)
		crate.collision_mask = WORLD
		crates.append(crate)


func _physics_process(_delta: float) -> bool:
	tick += 1
	var space := root.world_2d.direct_space_state
	if tick == 30:
		check(not queries.has_line_of_sight(space, Vector2(100, 200), Vector2(500, 200), WORLD),
			"the wall blocks the line from (100, 200) to (500, 200)")
		check(queries.has_line_of_sight(space, Vector2(100, 120), Vector2(500, 120), WORLD),
			"the line from (100, 120) to the player at (500, 120) is clear")
		check(queries.has_line_of_sight(space, Vector2(600, 280), Vector2(950, 280), WORLD),
			"crates do not block a line that only the world's layer may block")
		var down := [queries.ground_distance(space, Vector2(500, 100), 500.0, WORLD),
			queries.ground_distance(space, Vector2(300, 100), 500.0, WORLD),
			queries.ground_distance(space, Vector2(1200, 100), 500.0, WORLD)]
		check(is_equal_approx(down[0], 200.0), "the ground is 200 px below (500, 100): %.1f" % down[0])
		check(is_equal_approx(down[1], 50.0), "the wall's top is 50 px below (300, 100): %.1f" % down[1])
		check(down[2] == -1.0, "no ground within 500 px below (1200, 100): %.1f" % down[2])
		var pushed: int = queries.explode(space, Vector2(650, 285), 200.0, 600.0, CRATES)
		check(pushed == 2, "a blast of radius 200 at (650, 285) pushes the two crates within it: %d" % pushed)
	elif tick == 31:
		var speeds: Array = crates.map(func(c: RigidBody2D) -> float: return c.linear_velocity.length())
		check(speeds[0] > speeds[1] + 50.0, "the crate 50 px from the blast moves faster than the one 110 px away: %.1f and %.1f" % [
			speeds[0], speeds[1]])
		check(speeds[2] < 1.0, "the crate 250 px away does not move: %.1f" % speeds[2])
		print("%d of %d checks passed" % [passed, total])
		quit(0 if passed == total else 1)
	return false


func check(ok: bool, what: String) -> void:
	total += 1
	passed += int(ok)
	print("%s %s" % ["PASS" if ok else "FAIL", what])

The checker builds a floor, a wall, the player and three crates, asks for three sight lines and three ground distances, and sets off a blast between the crates. On a finished queries.gd, and on a tempting one whose rays keep the default mask and whose blast pushes every crate with the same strength:

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

# The checker on a finished queries.gd, then on a tempting one whose rays use the default mask and whose blast has no
# falloff.
files("practice/project.godot=right/project.godot", "practice/check_queries.gd=right/check_queries.gd",
      "practice/solution/queries.gd=right/queries.gd",
      "practice/project.godot=tempting/project.godot", "practice/check_queries.gd=tempting/check_queries.gd",
      "practice/tempting/queries.gd=tempting/queries.gd")
run("""
cd right
godot --headless --no-header --fixed-fps 60 --script check_queries.gd
cd ../tempting
!godot --headless --no-header --fixed-fps 60 --script check_queries.gd
""")

Output

$ cd right
$ godot --headless --no-header --fixed-fps 60 --script check_queries.gd
PASS the wall blocks the line from (100, 200) to (500, 200)
PASS the line from (100, 120) to the player at (500, 120) is clear
PASS crates do not block a line that only the world's layer may block
PASS the ground is 200 px below (500, 100): 200.0
PASS the wall's top is 50 px below (300, 100): 50.0
PASS no ground within 500 px below (1200, 100): -1.0
PASS a blast of radius 200 at (650, 285) pushes the two crates within it: 2
PASS the crate 50 px from the blast moves faster than the one 110 px away: 430.5 and 252.5
PASS the crate 250 px away does not move: 0.0
9 of 9 checks passed
$ cd ../tempting
$ godot --headless --no-header --fixed-fps 60 --script check_queries.gd
PASS the wall blocks the line from (100, 200) to (500, 200)
FAIL the line from (100, 120) to the player at (500, 120) is clear
FAIL crates do not block a line that only the world's layer may block
PASS the ground is 200 px below (500, 100): 200.0
PASS the wall's top is 50 px below (300, 100): 50.0
PASS no ground within 500 px below (1200, 100): -1.0
PASS a blast of radius 200 at (650, 285) pushes the two crates within it: 2
FAIL the crate 50 px from the blast moves faster than the one 110 px away: 579.4 and 581.2
PASS the crate 250 px away does not move: 0.0
6 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 tempting version saw the player’s own body and a crate as blockers, and its blast sent the near crate and the far one off at the same speed, 579.4 and 581.2 px a second. With all 9 checks passing, move on to the self-check below, which picks its questions from the five sections by your weakest answers and gives the reason for every choice.

Practice that adapts to you

Practise this lesson

15 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 15 What does the RigidBody2D reference mean when it calls an impulse "time-independent"?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: It changes the speed at once by impulse / mass, whatever the tick's length

    Forces are time dependent: their effect grows with the time they are applied.

  2. Question 2 of 15 The same impulse gave a 2 kg crate half the speed of a 1 kg crate. Why did it slide about a quarter as far?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: Friction slowed both by the same amount each tick, so the distance grows with the square of the speed

    A slide that loses a fixed speed each tick covers v² / (2a): half the speed, a quarter of the distance.

  3. Question 3 of 15 A blast should send a 1 kg crate and a 4 kg crate off at the same speed, 400 px a second. Which impulses do that?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: 400 for the 1 kg crate and 1,600 for the 4 kg crate

    Multiply the impulse by the mass, or set the speed through the impulse: mass × wanted velocity.

  4. Question 4 of 15 What does intersect_ray() return when the ray hits nothing?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: An empty dictionary

    Test with is_empty(), then read collider, position and normal.

  5. Question 5 of 15 Where does the Ray-casting page say a script should read get_world_2d().direct_space_state?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: In _physics_process(), because outside it the space may be locked

    Input handlers that need a query store the request and answer it in the next _physics_process().

  6. Question 6 of 15 A RayCast2D under a hero has target_position = Vector2(0, 400). The hero walks off a cliff 600 px high. What does is_colliding() say, and why?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: false, because the ray reaches only 400 px below the hero

    A probe's length is the furthest it looks; ground_distance() in the practice returns -1.0 for the same case.

  7. Question 7 of 15 Which layers does PhysicsRayQueryParameters2D.create(from, to) look at?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: Every layer, a mask of 4294967295

    Give queries the layers they mean, as the third argument of create().

  8. Question 8 of 15 A bullet's ray should ignore every other bullet in the level. Exclude list or mask?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: Leave the bullets' layer out of the mask

    Exclude the few single bodies that share a layer the query needs, such as the shooter.

  9. Question 9 of 15 A hit dictionary has a normal of (0, 0). What happened?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: The ray started inside the shape it reports, with hit_from_inside on

    Code that bounces or slides along a hit's normal must handle (0, 0) when it enables hit_from_inside.

  10. Question 10 of 15 A ShapeCast2D with target_position (400, 0) reports a closest safe fraction of 0.422. What does that mean?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: The shape can move 42.2 % of the way, 168.8 px, before it touches something

    A safe fraction of 1.000 means nothing stands in the way of the whole motion, as for the 20 px box.

  11. Question 11 of 15 cast_motion() returned [0.421875, 0.42578125] for the 40 px box. Why two numbers?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: The first is the furthest safe point, the second the first point of contact; the true contact lies between

    Move a body by the safe fraction to stop it just short of what it would hit.

  12. Question 12 of 15 A blast should push near crates harder than far ones. After intersect_shape() returns the crates, what does each impulse need?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The direction from the centre to the crate, times the strength, times (1 - distance / radius)

    Skip a crate exactly at the centre, whose direction is undefined, or push it in a fixed direction.

  13. Question 13 of 15 What did running the crate chain at 120 ticks a second instead of 60 do?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Every crate came to rest about half a pixel further, at 152.723 instead of 152.230 for the first

    Keep the tick rate fixed in tests and in anything that compares results.

  14. Question 14 of 15 Adding the same crates to the tree in the opposite order moved them by 3 to 5 thousandths of a pixel. What does that tell you?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: Changes that alter no number in the scene can still change a result, so an identical setup means the same order of events too

    Tests that compare physics results to many decimals build their scenes the same way every time.

  15. Question 15 of 15 A test checks that the first crate comes to rest at x = 152.230. Which assertion makes it robust without losing its meaning?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Check the position within a tolerance, such as 0.5 px, with the tick rate fixed by --fixed-fps and the scene built the same way

    Choose tolerances from what the game must guarantee, and pin everything that changes the simulation.

Interview questions

Warm-up: what is the difference between an impulse and a force on a RigidBody2D?

An impulse changes the body’s velocity at once, by the impulse divided by its mass, and Godot’s RigidBody2D reference calls it time-independent: one call to apply_central_impulse() is a kick, such as a jump, a hit or an explosion. A force changes the velocity a little on every tick it is applied, by the force divided by the mass times the tick’s length, so apply_central_force() is meant to be called every physics update, for thrust, wind or a magnet. In a Godot 4.7.2 run, an impulse of 400 gave a 1 kg crate 383 px a second one tick later, while a force of 1,500 on the same kind of crate took 30 ticks to build up 296.6 px a second, because friction fought it on every tick.

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

  1. Fresher to mid

    Enemies in a stealth game sometimes see the player through walls, and sometimes fail to see a player standing in the open in front of them, half hidden by a crate. Each enemy casts one ray to the player's centre with the default query parameters. Diagnose both bugs and design a better sight check.

    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 physics puzzle game stores replays as the player's inputs and re-simulates them. Replays look right on the developer's machine, but players report that boxes end up elsewhere, especially after a patch that raised the physics tick rate. Explain what is happening and design replays and tests that hold up.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A RigidBody2D is steered by impulses, which change its speed at once by impulse ÷ mass, and forces, which change it a little each tick, never by setting its position; at rest it falls asleep after 0.5 s and wakes when pushed.
  • A ray query returns the first collider on a line with its position and normal, or an empty dictionary, and a RayCast2D node keeps its answer until the next physics frame or force_raycast_update().
  • A query’s mask names the layers that count, exclude removes single bodies such as the caster, and hit_from_inside and collide_with_areas are off unless a query asks for them.
  • A shape cast sweeps a whole shape and reports how far it can move, which answers whether a body fits, and intersect_shape() lists what a shape overlaps.
  • Physics results repeat only with the same build, inputs, tick rate and order of events: in the run, doubling the tick rate or reversing the order of the crates changed where they came to rest.

Terms in this lesson

impulse
A push that changes a body's speed at once, by the impulse divided by the mass, whatever the tick's length. apply_central_impulse(Vector2(400, 0)) gave a 1 kg crate 400 px a second and a 4 kg crate 100.
shape cast
A query that sweeps a whole shape along a motion and reports where it would first touch something, as a fraction of the motion. Unlike a ray, it knows whether a body of that size fits.
sleeping body
A RigidBody2D that has rested for time_before_sleep, 0.5 s by default, and that the engine stops simulating until a collision, an impulse or code wakes it. It then acts like a static body.
space query
A question asked of the physics world through PhysicsDirectSpaceState2D: what a ray hits, what a shape overlaps, how far a shape can move. It answers at once, and the docs say to ask it in _physics_process().

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.