Technical Notes
Edge conventions, scale rules and performance facts that the guides assume. All of these are the library's actual behavior, stated precisely.
EDGE CONVENTIONS
- Polygon queries are half-open on right and bottom edges. A sprite sitting exactly on a tile's right or bottom boundary does not count as colliding; the left and top edges are inclusive. This is what lets two solids rest flush without false positives.
- Object-manager AABB checks are inclusive: touching bounding boxes in
ObjectCollisionManagercount as overlapping, soresolve()pushes apart with zero overlap.
RENDER_SCALE
TilemapData.render_scaleis adopted byPhysicsWorldand by the runner on attach; rendering and collision stay in the same pixel space.- Effective tile size is
tile_size × render_scale; tile(col, row)occupies(col × eff_w, row × eff_h). Arender_scale ≤ 0raisesValueErrorin both the renderer and the runner. - Polygon vertices are scaled by
render_scalewhen transformed to world space; author collision in tile-local pixels, scale for free.
BODY MODES ARE PROMISES
"static" and "kinematic" do not imply physics-engine dynamics. Velocity is scripted, Godot StaticBody2D/CharacterBody2D style. A kinematic body moves only when your loop drives it through a move_* call with explicit velocity.
ONE-WAY, PRECISELY
- Only
move_platformerandmove_platformer_with_slidehonorone_waypolygons (block from above, pass from below). - The flag is authored in the collision JSON, per polygon — never in the map JSON, and never auto-detected. Control is authoring-level only: there is no runtime per-sprite "make platforms solid" override. Your levers are the JSON flag and the movement method you call.
- Gating is approach-based: a one-way polygon blocks only when the sprite is falling (
vy > 0) and its previous bottom was above the platform top — fast falls can't tunnel, and jumping up through always passes. - The horizontal phase never sees one-way polygons (they can't wall you); the landing snap re-includes them with from-above gating.
move_groundedtreats one-way polygons as plain solid geometry.- Bodies are never one-way; they block from every direction.
- The renderer doesn't know the flag exists: one-way tiles draw like any other tile. For dashed-edge overlays, query
world.tile_map+ the collision tileset at runtime (see the Map Parsing page).
VELOCITY CONTRACT, RECAP
Physics modes with velocity=(vx, vy): skip gravity/input/jump, adopt the velocity onto the sprite, zero vx on wall hit, zero vy on landing. Displacement modes (move_and_slide and move_rpg) never read or write vx/vy.
PERFORMANCE NOTES
- Movement queries are zero-allocation on the hot path.
_collides_atiterates tiles and shapes inline and exits on the first hit;get_nearby_tile_shapes(which allocates) exists for your own queries, not for movement. - The runner reuses one
CollisionResult. Fields are reset per call. Read the return value in the same frame. - Rendering is chunk-culled (32×32-tile chunks), with per-layer
z_indexsorting and a tile-variant cache.warm_cache()pre-bakes variants and frees the source map data. - Object collision uses a uniform spatial grid rebuilt per
check_all_collisions(); single-object queries are a linear scan.cell_size(default 128) is the tuning knob; benchmark withexamples/comparison/spatial-cell-size-tuning.py. - Circle/capsule bodies are polygon-approximated (16-edge ngon / stepped capsule) only for slide-mode normal computation; the tile resolver works on polygon edges.
CAMERA FACTS
mode="centered": target always at viewport center (lerp iflerp_speed > 0).mode="deadzone": camera only moves when the target leaves a centered box.shake(duration, intensity)adds a random per-frame offset insideoffset; it does not move the target.bounds = (min_x, min_y, max_x, max_y)clamps the camera position.
THE WORLD OWNS, THE RUNNER RESOLVES
One space, one runner. Tiles and bodies are solids in the same space; the runner resolves movement against the union. The world does not simulate sprite-vs-sprite contact; that's ObjectCollisionManager. If you can draw it, you can move it; if you can move it, it can be pushed.
GID OWNERSHIP IS A RANGE TEST, NOT A SUBTRACTION
With use_gids=True, tile ids are firstgid + local_variant per grid resource. Collision files stay local-keyed. Resolving a gid therefore never does a blind gid - firstgid_of_owner: the world first finds which grid resource's window [firstgid, firstgid+count) owns the id, rejects ids owned by non-collision resources (decoration grids), and only then subtracts. This kills cross-tileset aliasing — e.g. gid 1813 - 90 = 1723 must not light up jungle-local key 1723. See GID routing on the runner page.