Skip to content

Defer spatial hash updates until the next query - #2977

Merged
pvcraven merged 1 commit into
developmentfrom
deferred-spatial-hash
Oct 9, 2026
Merged

pvcraven merged 1 commit into
developmentfrom
deferred-spatial-hash

Conversation

@pvcraven

@pvcraven pvcraven commented Oct 9, 2026

Copy link
Copy Markdown
Member

Closes #1568

#1568 had four parts. Two were done in #2894–#2900: cached hit box radius, bounds and axes, and HitBox as its own class. Sharing one hit box per texture is worth little now that those caches exist, so it's left out. This PR does the remaining valid part, deferring spatial hash updates when sprites move.

The problem

Each change to center_x, center_y, angle, scale or texture of a sprite in a hashed SpriteList called SpatialHash.move(), which removed the sprite and added it again. Each add also forced the hit box's adjusted points to be recalculated. Moving a sprite's x and then its y re-added it twice per frame, even if it stayed in the same cells and nothing ever checked for collisions.

The change (all in arcade/sprite_list/spatial_hash.py)

  • move() only adds the sprite to a set of moved sprites.
  • The query methods (get_sprites_near_sprite, get_sprites_near_point, get_sprites_near_rect) first put each moved sprite in its new cells. A sprite whose cell range hasn't changed is skipped, using a stored (min, max) cell pair per sprite.
  • remove() drops a sprite from the moved set, and reset() clears it.
  • contents and buckets_for_sprite are now properties that apply pending moves first, so code reading them, including the existing tests, sees current data. The class keeps the same public names, so nothing breaks.

Sprite code is unchanged; it still calls update_spatial_hash(). Rendering doesn't go through the hash, so drawing isn't affected.

Measured (5,000 16×16 sprites in a 1000×1000 area, default cell size)

ms per frame Before After
Move every sprite (center_x += 1, center_y += 1) 36.1 8.1
Same, then one check_for_collision_with_list 39.1 22.4
Tiny move that stays in the same cells 20.2 4.1
Unhashed list, move every sprite (for comparison) 5.9 5.9

Collision checks with nothing moving cost the same: three alternating runs gave 92–97 µs per check both before and after.

Also

  • The performance tips said spatial hashing "doubles the cost of moving or resizing sprites". It was about 6× before this PR. The section now explains the deferred updates and gives the measured numbers.
  • benchmarks/spatial_hash/add_remove_vs_move.py used insert_object_for_box and remove_object, which no longer exist. It now uses add/remove, and queries once after moving so the moves are applied.

Tests

tests/unit/spritelist/test_spatial_hash_moves.py:

  • queries and collision checks see moved sprites
  • three moves cause one re-add
  • a move within the same cells causes no re-add
  • removing after moving works
  • contents and buckets_for_sprite reflect moves
  • reset() forgets pending moves

On development, the two tests that measure re-adds fail and the other five pass.

The full test suite (1858), ruff, mypy, pyright and make.py docs-full pass. 8 examples that use spatial hashes run without errors: platformers, walls, rooms, moving platforms, the bullet sweep and the camera platformer.

🤖 Generated with Claude Code

Moving, rotating or resizing a sprite in a hashed SpriteList removed it
from the spatial hash and added it again, on every change. SpatialHash
now only marks it as moved; the next query updates each moved sprite
once and skips sprites still in the same cells. Moving 5,000 hashed
sprites went from 36 to 8 ms per frame (39 to 22 ms with a collision
check after); queries with nothing moving cost the same.

contents and buckets_for_sprite become properties that apply pending
moves first, so code reading them still sees current data.

Also corrects the performance tips, which said hashing doubles the cost
of moving, and fixes a benchmark that used removed method names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pvcraven
pvcraven force-pushed the deferred-spatial-hash branch from a145e98 to 3d0c5f1 Compare October 9, 2026 21:26
@pvcraven
pvcraven merged commit b92dfe2 into development Oct 9, 2026
7 checks passed
@pvcraven
pvcraven deleted the deferred-spatial-hash branch October 9, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor hitboxes, adjusted hitboxes, and optimize their usage within spatial hashes

1 participant