diff --git a/CHANGELOG.md b/CHANGELOG.md index e478b5811..8d589309a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - Fixed `InputManager.parse()` raising `AttributeError`, so a saved input configuration couldn't be loaded. ### Breaking Changes +- `arcade.load_tilemap()` and `TileMap` now give every layer's sprite list a spatial hash by default (`use_spatial_hash=True`) ([#2663](https://github.com/pythonarcade/arcade/issues/2663)). Forgetting it on a wall layer made collision checks against it much slower: 100 bullets checked against 2,000 wall tiles took 48 ms per frame without it and 0.6 ms with it. A hash makes moving a layer's sprites slower, so for a layer whose sprites move a lot, such as moving platforms, turn it off with `layer_options={"Moving Platforms": {"use_spatial_hash": False}}`, or for the whole map with `use_spatial_hash=False`. + - `layer_options` now warns about option names it doesn't know, which it used to ignore. The performance tips page named the option `"use_spatial_hashing"`; it's `"use_spatial_hash"`. - `Sprite`, `BasicSprite`, `SpriteSolidColor`, `SpriteCircle`, `TextureAnimationSprite` and `AnimatedWalkingSprite` raise `TypeError` for keyword arguments they don't accept, instead of silently ignoring them. For example, `Sprite(path, hit_box_algorithm=...)` used to do nothing; set the hit box algorithm when loading the texture instead. `Sprite` now takes `visible` as a named argument. - `SpriteSolidColor`, `SpriteCircle`, `TextureAnimationSprite` and `AnimatedWalkingSprite` now pass extra keyword arguments on to `Sprite`, so arguments such as `angle` and `visible` work. They used to be dropped. - This found arguments that had silently done nothing: tilemap image layers passed `filename` and `hit_box_algorithm`, animated tiles passed `path_or_texture`, and platform tutorial steps 19 and 20 passed `scaling` instead of `scale`, so their bullets were drawn at full size instead of 80%. diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index cd3580d97..0b717f719 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -12,6 +12,7 @@ import copy import math import os +import warnings from collections import OrderedDict from collections.abc import Callable from pathlib import Path @@ -149,10 +150,10 @@ class TileMap: layer_options: Extra parameters for each layer. use_spatial_hash: - If set to True, this will make moving a sprite - in the SpriteList slower, but it will speed up collision detection - with items in the SpriteList. Great for doing collision detection - with static walls/platforms. + Give each layer's SpriteList a spatial hash, which makes collision + checks against it much faster, and moving its sprites slower. On by + default. For a layer whose sprites move a lot, such as moving + platforms, turn it off in ``layer_options``. hit_box_algorithm: The hit box algorithm to use for the Sprite's in this layer. tiled_map: @@ -179,6 +180,7 @@ class TileMap: The available options for this are: - ``use_spatial_hash`` - A boolean to enable spatial hashing on this layer's SpriteList. + On by default. Turn it off for a layer whose sprites move a lot. - ``scaling`` - A float providing layer specific Sprite scaling. - ``hit_box_algorithm`` - The hit box algorithm to use for the Sprite's in this layer. - ``offset`` - A tuple containing X and Y position offsets for the layer @@ -248,7 +250,7 @@ def __init__( map_file: str | Path = "", scaling: float = 1.0, layer_options: dict[str, dict[str, Any]] | None = None, - use_spatial_hash: bool = False, + use_spatial_hash: bool = True, hit_box_algorithm: HitBoxAlgorithm | None = None, tiled_map: pytiled_parser.TiledMap | None = None, offset: Vec2 = Vec2(0, 0), @@ -316,6 +318,18 @@ def __init__( "texture_atlas": texture_atlas, } + # Unknown options would otherwise be silently ignored, such as a + # misspelled "use_spatial_hashing" + for layer_name, options in (layer_options or {}).items(): + unknown = sorted(set(options) - set(global_options)) + if unknown: + warnings.warn( + f"Unknown layer_options for layer {layer_name!r}: {', '.join(unknown)}. " + f"The options are: {', '.join(global_options)}.", + # Point at the code calling load_tilemap(), the usual way in + stacklevel=3, + ) + for layer in self.tiled_map.layers: if (layer.name in self.sprite_lists) or (layer.name in self.object_lists): raise AttributeError( @@ -1108,7 +1122,7 @@ def load_tilemap( map_file: str | Path, scaling: float = 1.0, layer_options: dict[str, dict[str, Any]] | None = None, - use_spatial_hash: bool = False, + use_spatial_hash: bool = True, hit_box_algorithm: HitBoxAlgorithm | None = None, offset: Vec2 = Vec2(0, 0), texture_atlas: DefaultTextureAtlas | None = None, @@ -1130,10 +1144,10 @@ def load_tilemap( scaling: The global scaling to apply to all Sprite's within the map. use_spatial_hash: - If set to True, this will make moving a sprite - in the SpriteList slower, but it will speed up collision detection - with items in the SpriteList. Great for doing collision detection - with static walls/platforms. + Give each layer's SpriteList a spatial hash, which makes collision + checks against it much faster, and moving its sprites slower. On by + default. For a layer whose sprites move a lot, such as moving + platforms, turn it off in ``layer_options``. hit_box_algorithm: The hit box algorithm to use for collision detection. layer_options: diff --git a/doc/programming_guide/performance_tips.rst b/doc/programming_guide/performance_tips.rst index 525df2a05..f4ede19d8 100644 --- a/doc/programming_guide/performance_tips.rst +++ b/doc/programming_guide/performance_tips.rst @@ -197,21 +197,24 @@ of 3 pixel sprites. Spatial Hashing and Tiled Maps """""""""""""""""""""""""""""" -There is also a way to enable spatial hashing when loading Tiled maps. For -each layer you'd like to load with spatial hashing, set a ``"use_spatial_hashing"`` -key in its layer options to ``True``: +Layers loaded from Tiled maps with :py:func:`arcade.load_tilemap` get a +spatial hash by default, so collision checks against walls and platforms +are fast. For a layer whose sprites move a lot, such as moving platforms, +turn it off with the ``"use_spatial_hash"`` key in its layer options: .. code-block:: python layer_options = { - "ground": { - "use_spatial_hash": True - }, - "non_moving_platforms": { - "use_spatial_hash": True + "moving_platforms": { + "use_spatial_hash": False } } + tile_map = arcade.load_tilemap("my_map.json", layer_options=layer_options) + +``load_tilemap`` warns about layer option names it doesn't know, such as a +misspelled ``"use_spatial_hashing"``. + For a runnable example of this, please see :ref:`camera_platform`. Additional examples are linked below in :ref:`collision_performance_spatial_hashing_examples`. diff --git a/doc/tutorials/platform_tutorial/step_12.rst b/doc/tutorials/platform_tutorial/step_12.rst index 6357a90c1..9a0d61982 100644 --- a/doc/tutorials/platform_tutorial/step_12.rst +++ b/doc/tutorials/platform_tutorial/step_12.rst @@ -88,8 +88,10 @@ at this stage is ready for drawing and we don't need to do anything else to it(o ``layer_options`` is a special dictionary that can be provided to the ``load_tilemap`` function. This will send special options for each layer into the map loader. In this example our map has a layer called - ``Platforms``, and we want to enable spatial hashing on it. Much like we did for our ``wall`` SpriteList - before. For more info on the layer options dictionary and the available keys, check out :class:`arcade.TileMap` + ``Platforms``, and we want spatial hashing on it, much like we did for our ``wall`` SpriteList + before. Tile map layers get spatial hashing by default, so this option just makes that clear. You would + turn it off for a layer whose sprites move a lot. For more info on the layer options dictionary and + the available keys, check out :class:`arcade.TileMap` At this point we only have one piece of code left to change. In switching to our new map, you may have noticed by the ``layer_options`` dictionary that we now have a layer named ``Platforms``. Previously in our Scene we were calling diff --git a/tests/unit/tilemap/test_spatial_hash_default.py b/tests/unit/tilemap/test_spatial_hash_default.py new file mode 100644 index 000000000..33537c4eb --- /dev/null +++ b/tests/unit/tilemap/test_spatial_hash_default.py @@ -0,0 +1,42 @@ +"""Tile map layers get a spatial hash by default (#2663).""" + +import pytest + +import arcade + +MAP = ":resources:tiled_maps/map2_level_1.json" + + +def test_layers_have_spatial_hash_by_default(window): + tile_map = arcade.load_tilemap(MAP) + assert tile_map.sprite_lists + for sprite_list in tile_map.sprite_lists.values(): + assert sprite_list.spatial_hash is not None + + +def test_turn_off_for_one_layer(window): + tile_map = arcade.load_tilemap(MAP, layer_options={"Platforms": {"use_spatial_hash": False}}) + assert tile_map.sprite_lists["Platforms"].spatial_hash is None + assert tile_map.sprite_lists["Coins"].spatial_hash is not None + + +def test_turn_off_for_whole_map(window): + tile_map = arcade.load_tilemap(MAP, use_spatial_hash=False) + for sprite_list in tile_map.sprite_lists.values(): + assert sprite_list.spatial_hash is None + + +def test_unknown_layer_option_warns(window): + with pytest.warns(UserWarning, match="use_spatial_hashing"): + tile_map = arcade.load_tilemap( + MAP, layer_options={"Platforms": {"use_spatial_hashing": False}} + ) + # The misspelled option has no effect + assert tile_map.sprite_lists["Platforms"].spatial_hash is not None + + +def test_known_layer_options_dont_warn(window, recwarn): + arcade.load_tilemap( + MAP, layer_options={"Platforms": {"use_spatial_hash": True, "scaling": 2.0}} + ) + assert not [w for w in recwarn if "layer_options" in str(w.message)]