Improve TileMap performances by using quadrants only for rendering

This commit is contained in:
Gilles Roudière 2023-08-24 13:31:14 +02:00
parent 31a7fd1951
commit 8c1e28237b
6 changed files with 1405 additions and 1142 deletions

View file

@ -5,6 +5,8 @@
</brief_description>
<description>
Node for 2D tile-based maps. Tilemaps use a [TileSet] which contain a list of tiles which are used to create grid-based maps. A TileMap may have several layers, layouting tiles on top of each other.
For performance reasons, all TileMap updates are batched at the end of a frame. Notably, this means that scene tiles from a [TileSetScenesCollectionSource] may be initialized after their parent.
To force an update earlier on, call [method update_internals].
</description>
<tutorials>
<link title="Using Tilemaps">$DOCS_URL/tutorials/2d/using_tilemaps.html</link>
@ -25,7 +27,7 @@
Called with a TileData object about to be used internally by the TileMap, allowing its modification at runtime.
This method is only called if [method _use_tile_data_runtime_update] is implemented and returns [code]true[/code] for the given tile [param coords] and [param layer].
[b]Warning:[/b] The [param tile_data] object's sub-resources are the same as the one in the TileSet. Modifying them might impact the whole TileSet. Instead, make sure to duplicate those resources.
[b]Note:[/b] If the properties of [param tile_data] object should change over time, use [method force_update] to trigger a TileMap update.
[b]Note:[/b] If the properties of [param tile_data] object should change over time, use [method notify_runtime_tile_data_update] to notify the TileMap it needs an update.
</description>
</method>
<method name="_use_tile_data_runtime_update" qualifiers="virtual">
@ -35,6 +37,7 @@
<description>
Should return [code]true[/code] if the tile at coordinates [param coords] on layer [param layer] requires a runtime update.
[b]Warning:[/b] Make sure this function only return [code]true[/code] when needed. Any tile processed at runtime without a need for it will imply a significant performance penalty.
[b]Note:[/b] If the result of this function should changed, use [method notify_runtime_tile_data_update] to notify the TileMap it needs an update.
</description>
</method>
<method name="add_layer">
@ -73,13 +76,11 @@
Clears cells that do not exist in the tileset.
</description>
</method>
<method name="force_update">
<method name="force_update" is_deprecated="true">
<return type="void" />
<param index="0" name="layer" type="int" default="-1" />
<description>
Triggers an update of the TileMap. If [param layer] is provided and is positive, only updates the given layer.
[b]Note:[/b] The TileMap node updates automatically when one of its properties is modified. A manual update is only needed if runtime modifications (implemented in [method _tile_data_runtime_update]) need to be applied.
[b]Warning:[/b] Updating the TileMap is computationally expensive and may impact performance. Try to limit the number of updates and the tiles they impact (by placing frequently updated tiles in a dedicated layer for example).
[i]Deprecated.[/i] See [method notify_runtime_tile_data_update] and [method update_internals].
</description>
</method>
<method name="get_cell_alternative_tile" qualifiers="const">
@ -301,6 +302,16 @@
Moves the layer at index [param layer] to the given position [param to_position] in the array.
</description>
</method>
<method name="notify_runtime_tile_data_update">
<return type="void" />
<param index="0" name="layer" type="int" default="-1" />
<description>
Notifies the TileMap node that calls to [method _use_tile_data_runtime_update] or [method _tile_data_runtime_update] will lead to different results. This will thus trigger a TileMap update.
If [param layer] is provided, only notifies changes for the given layer. Providing the [param layer] argument (when applicable) is usually preferred for performance reasons.
[b]Warning:[/b] Updating the TileMap is computationally expensive and may impact performance. Try to limit the number of calls to this function to avoid unnecessary update.
[b]Note:[/b] This does not trigger a direct update of the TileMap, the update will be done at the end of the frame as usual (unless you call [method update_internals]).
</description>
</method>
<method name="remove_layer">
<return type="void" />
<param index="0" name="layer" type="int" />
@ -437,11 +448,16 @@
If [param layer] is negative, the layers are accessed from the last one.
</description>
</method>
<method name="update_internals">
<return type="void" />
<description>
Triggers a direct update of the TileMap. Usually, calling this function is not needed, as TileMap node updates automatically when one of its properties or cells is modified.
However, for performance reasons, those updates are batched and delayed to the end of the frame. Calling this function will force the TileMap to update right away instead.
[b]Warning:[/b] Updating the TileMap is computationally expensive and may impact performance. Try to limit the number of updates and how many tiles they impact.
</description>
</method>
</methods>
<members>
<member name="cell_quadrant_size" type="int" setter="set_quadrant_size" getter="get_quadrant_size" default="16">
The TileMap's quadrant size. Optimizes drawing by batching, using chunks of this size.
</member>
<member name="collision_animatable" type="bool" setter="set_collision_animatable" getter="is_collision_animatable" default="false">
If enabled, the TileMap will see its collisions synced to the physics tick and change its collision type from static to kinematic. This is required to create TileMap-based moving platform.
[b]Note:[/b] Enabling [member collision_animatable] may have a small performance impact, only do it if the TileMap is moving and has colliding tiles.
@ -452,6 +468,9 @@
<member name="navigation_visibility_mode" type="int" setter="set_navigation_visibility_mode" getter="get_navigation_visibility_mode" enum="TileMap.VisibilityMode" default="0">
Show or hide the TileMap's navigation meshes. If set to [constant VISIBILITY_MODE_DEFAULT], this depends on the show navigation debug settings.
</member>
<member name="rendering_quadrant_size" type="int" setter="set_rendering_quadrant_size" getter="get_rendering_quadrant_size" default="16">
The TileMap's quadrant size. Optimizes drawing by batching, using chunks of this size.
</member>
<member name="tile_set" type="TileSet" setter="set_tileset" getter="get_tileset">
The assigned [TileSet].
</member>

View file

@ -169,3 +169,10 @@ Validate extension JSON: API was removed: classes/GraphNode/signals/raise_reques
Validate extension JSON: API was removed: classes/GraphNode/signals/resize_request
Refactor GraphNode (splitup in GraphElement and GraphNode)
GH-81070
--------
Validate extension JSON: API was removed: classes/TileMap/methods/get_quadrant_size
Validate extension JSON: API was removed: classes/TileMap/methods/set_quadrant_size
Validate extension JSON: API was removed: classes/TileMap/properties/cell_quadrant_size
cell_quadrant_size/quadrant_size of the TileMap API was renamed to rendering_quadrant_size.

View file

@ -34,8 +34,18 @@ Rect2i TileMap::_get_used_rect_bind_compat_78328() {
return get_used_rect();
}
void TileMap::_set_quadrant_size_compat_81070(int p_quadrant_size) {
set_rendering_quadrant_size(p_quadrant_size);
}
int TileMap::_get_quadrant_size_compat_81070() const {
return get_rendering_quadrant_size();
}
void TileMap::_bind_compatibility_methods() {
ClassDB::bind_compatibility_method(D_METHOD("get_used_rect"), &TileMap::_get_used_rect_bind_compat_78328);
ClassDB::bind_compatibility_method(D_METHOD("set_quadrant_size", "quadrant_size"), &TileMap::_set_quadrant_size_compat_81070);
ClassDB::bind_compatibility_method(D_METHOD("get_quadrant_size"), &TileMap::_get_quadrant_size_compat_81070);
}
#endif

File diff suppressed because it is too large Load diff

View file

@ -89,7 +89,102 @@ public:
TerrainConstraint(){};
};
struct TileMapQuadrant {
#ifdef DEBUG_ENABLED
class DebugQuadrant;
#endif // DEBUG_ENABLED
class RenderingQuadrant;
struct CellData {
Vector2i coords;
TileMapCell cell;
// Debug.
SelfList<CellData> debug_quadrant_list_element;
// Rendering.
Ref<RenderingQuadrant> rendering_quadrant;
SelfList<CellData> rendering_quadrant_list_element;
List<RID> occluders;
// Physics.
LocalVector<RID> bodies;
// Navigation.
LocalVector<RID> navigation_regions;
// Scenes.
String scene;
// Runtime TileData cache.
TileData *runtime_tile_data_cache = nullptr;
// List elements.
SelfList<CellData> dirty_list_element;
// For those, copy everything but SelfList elements.
void operator=(const CellData &p_other) {
coords = p_other.coords;
cell = p_other.cell;
occluders = p_other.occluders;
bodies = p_other.bodies;
navigation_regions = p_other.navigation_regions;
scene = p_other.scene;
runtime_tile_data_cache = p_other.runtime_tile_data_cache;
}
CellData(const CellData &p_other) :
debug_quadrant_list_element(this),
rendering_quadrant_list_element(this),
dirty_list_element(this) {
coords = p_other.coords;
cell = p_other.cell;
occluders = p_other.occluders;
bodies = p_other.bodies;
navigation_regions = p_other.navigation_regions;
scene = p_other.scene;
runtime_tile_data_cache = p_other.runtime_tile_data_cache;
}
CellData() :
debug_quadrant_list_element(this),
rendering_quadrant_list_element(this),
dirty_list_element(this) {
}
};
#ifdef DEBUG_ENABLED
class DebugQuadrant : public RefCounted {
GDCLASS(DebugQuadrant, RefCounted);
public:
Vector2i quadrant_coords;
SelfList<CellData>::List cells;
RID canvas_item;
SelfList<DebugQuadrant> dirty_quadrant_list_element;
// For those, copy everything but SelfList elements.
DebugQuadrant(const DebugQuadrant &p_other) :
dirty_quadrant_list_element(this) {
quadrant_coords = p_other.quadrant_coords;
cells = p_other.cells;
canvas_item = p_other.canvas_item;
}
DebugQuadrant() :
dirty_quadrant_list_element(this) {
}
~DebugQuadrant() {
cells.clear();
}
};
#endif // DEBUG_ENABLED
class RenderingQuadrant : public RefCounted {
GDCLASS(RenderingQuadrant, RefCounted);
public:
struct CoordsWorldComparator {
_ALWAYS_INLINE_ bool operator()(const Vector2 &p_a, const Vector2 &p_b) const {
// We sort the cells by their local coords, as it is needed by rendering.
@ -101,63 +196,32 @@ struct TileMapQuadrant {
}
};
// Dirty list element.
SelfList<TileMapQuadrant> dirty_list_element;
// Quadrant coords.
Vector2i coords;
// TileMapCells.
RBSet<Vector2i> cells;
// We need those two maps to sort by local position for rendering.
// This is kind of workaround, it would be better to sort the cells directly in the "cells" set instead.
RBMap<Vector2i, Vector2> map_to_local;
RBMap<Vector2, Vector2i, CoordsWorldComparator> local_to_map;
// Debug.
RID debug_canvas_item;
// Rendering.
Vector2i quadrant_coords;
SelfList<CellData>::List cells;
List<RID> canvas_items;
HashMap<Vector2i, RID> occluders;
// Physics.
List<RID> bodies;
SelfList<RenderingQuadrant> dirty_quadrant_list_element;
// Navigation.
HashMap<Vector2i, Vector<RID>> navigation_regions;
// Scenes.
HashMap<Vector2i, String> scenes;
// Runtime TileData cache.
HashMap<Vector2i, TileData *> runtime_tile_data_cache;
void operator=(const TileMapQuadrant &q) {
coords = q.coords;
debug_canvas_item = q.debug_canvas_item;
canvas_items = q.canvas_items;
occluders = q.occluders;
bodies = q.bodies;
navigation_regions = q.navigation_regions;
// For those, copy everything but SelfList elements.
RenderingQuadrant(const RenderingQuadrant &p_other) :
dirty_quadrant_list_element(this) {
quadrant_coords = p_other.quadrant_coords;
cells = p_other.cells;
canvas_items = p_other.canvas_items;
}
TileMapQuadrant(const TileMapQuadrant &q) :
dirty_list_element(this) {
coords = q.coords;
debug_canvas_item = q.debug_canvas_item;
canvas_items = q.canvas_items;
occluders = q.occluders;
bodies = q.bodies;
navigation_regions = q.navigation_regions;
RenderingQuadrant() :
dirty_quadrant_list_element(this) {
}
TileMapQuadrant() :
dirty_list_element(this) {
~RenderingQuadrant() {
cells.clear();
}
};
class TileMapLayer : public RefCounted {
GDCLASS(TileMapLayer, RefCounted);
public:
enum DataFormat {
FORMAT_1 = 0,
@ -166,6 +230,34 @@ public:
FORMAT_MAX,
};
enum DirtyFlags {
DIRTY_FLAGS_LAYER_ENABLED = 0,
DIRTY_FLAGS_LAYER_MODULATE,
DIRTY_FLAGS_LAYER_Y_SORT_ENABLED,
DIRTY_FLAGS_LAYER_Y_SORT_ORIGIN,
DIRTY_FLAGS_LAYER_Z_INDEX,
DIRTY_FLAGS_LAYER_INDEX_IN_TILE_MAP_NODE,
DIRTY_FLAGS_TILE_MAP_IN_TREE,
DIRTY_FLAGS_TILE_MAP_IN_CANVAS,
DIRTY_FLAGS_TILE_MAP_VISIBILITY,
DIRTY_FLAGS_TILE_MAP_XFORM,
DIRTY_FLAGS_TILE_MAP_LOCAL_XFORM,
DIRTY_FLAGS_TILE_MAP_SELECTED_LAYER,
DIRTY_FLAGS_TILE_MAP_LIGHT_MASK,
DIRTY_FLAGS_TILE_MAP_MATERIAL,
DIRTY_FLAGS_TILE_MAP_USE_PARENT_MATERIAL,
DIRTY_FLAGS_TILE_MAP_TEXTURE_FILTER,
DIRTY_FLAGS_TILE_MAP_TEXTURE_REPEAT,
DIRTY_FLAGS_TILE_MAP_TILE_SET,
DIRTY_FLAGS_TILE_MAP_QUADRANT_SIZE,
DIRTY_FLAGS_TILE_MAP_COLLISION_ANIMATABLE,
DIRTY_FLAGS_TILE_MAP_COLLISION_VISIBILITY_MODE,
DIRTY_FLAGS_TILE_MAP_NAVIGATION_VISIBILITY_MODE,
DIRTY_FLAGS_TILE_MAP_Y_SORT_ENABLED,
DIRTY_FLAGS_TILE_MAP_RUNTIME_UPDATE,
DIRTY_FLAGS_MAX,
};
private:
// Exposed properties.
String name;
@ -181,10 +273,14 @@ private:
TileMap *tile_map_node = nullptr;
int layer_index_in_tile_map_node = -1;
RID canvas_item;
bool _rendering_quadrant_order_dirty = false;
HashMap<Vector2i, TileMapCell> tile_map;
HashMap<Vector2i, TileMapQuadrant> quadrant_map;
SelfList<TileMapQuadrant>::List dirty_quadrant_list;
HashMap<Vector2i, CellData> tile_map;
// Dirty flag. Allows knowing what was modified since the last update.
struct {
bool flags[DIRTY_FLAGS_MAX] = { false };
SelfList<CellData>::List cell_list;
} dirty;
bool in_destructor = false;
// Rect cache.
mutable Rect2 rect_cache;
@ -192,40 +288,57 @@ private:
mutable Rect2i used_rect_cache;
mutable bool used_rect_cache_dirty = true;
// Quadrants management.
Vector2i _coords_to_quadrant_coords(const Vector2i &p_coords) const;
HashMap<Vector2i, TileMapQuadrant>::Iterator _create_quadrant(const Vector2i &p_qk);
void _make_quadrant_dirty(HashMap<Vector2i, TileMapQuadrant>::Iterator Q);
void _erase_quadrant(HashMap<Vector2i, TileMapQuadrant>::Iterator Q);
// Runtime tile data.
bool _runtime_update_tile_data_was_cleaned_up = false;
void _build_runtime_update_tile_data();
void _build_runtime_update_tile_data_for_cell(CellData &r_cell_data, bool p_auto_add_to_dirty_list = false);
void _clear_runtime_update_tile_data();
// Per-system methods.
void _rendering_notification(int p_what);
#ifdef DEBUG_ENABLED
HashMap<Vector2i, Ref<DebugQuadrant>> debug_quadrant_map;
Vector2i _coords_to_debug_quadrant_coords(const Vector2i &p_coords) const;
bool _debug_was_cleaned_up = false;
void _debug_update();
void _debug_quadrants_update_cell(CellData &r_cell_data, SelfList<DebugQuadrant>::List &r_dirty_debug_quadrant_list);
#endif // DEBUG_ENABLED
HashMap<Vector2i, Ref<RenderingQuadrant>> rendering_quadrant_map;
Vector2i _coords_to_rendering_quadrant_coords(const Vector2i &p_coords) const;
bool _rendering_was_cleaned_up = false;
void _rendering_update();
void _rendering_cleanup();
void _rendering_update_dirty_quadrants(SelfList<TileMapQuadrant>::List &r_dirty_quadrant_list);
void _rendering_reorder_quadrants(int &r_index);
void _rendering_create_quadrant(TileMapQuadrant *p_quadrant);
void _rendering_cleanup_quadrant(TileMapQuadrant *p_quadrant);
void _rendering_draw_quadrant_debug(TileMapQuadrant *p_quadrant);
void _rendering_quadrants_update_cell(CellData &r_cell_data, SelfList<RenderingQuadrant>::List &r_dirty_rendering_quadrant_list);
void _rendering_occluders_clear_cell(CellData &r_cell_data);
void _rendering_occluders_update_cell(CellData &r_cell_data);
#ifdef DEBUG_ENABLED
void _rendering_draw_cell_debug(const RID &p_canvas_item, const Vector2i &p_quadrant_pos, const CellData &r_cell_data);
#endif // DEBUG_ENABLED
HashMap<RID, Vector2i> bodies_coords; // Mapping for RID to coords.
void _physics_update_dirty_quadrants(SelfList<TileMapQuadrant>::List &r_dirty_quadrant_list);
void _physics_cleanup_quadrant(TileMapQuadrant *p_quadrant);
void _physics_draw_quadrant_debug(TileMapQuadrant *p_quadrant);
bool _physics_was_cleaned_up = false;
void _physics_update();
void _physics_notify_tilemap_change(DirtyFlags p_what);
void _physics_clear_cell(CellData &r_cell_data);
void _physics_update_cell(CellData &r_cell_data);
#ifdef DEBUG_ENABLED
void _physics_draw_cell_debug(const RID &p_canvas_item, const Vector2i &p_quadrant_pos, const CellData &r_cell_data);
#endif // DEBUG_ENABLED
bool _navigation_was_cleaned_up = false;
void _navigation_update();
void _navigation_cleanup();
void _navigation_update_dirty_quadrants(SelfList<TileMapQuadrant>::List &r_dirty_quadrant_list);
void _navigation_cleanup_quadrant(TileMapQuadrant *p_quadrant);
void _navigation_draw_quadrant_debug(TileMapQuadrant *p_quadrant);
void _navigation_clear_cell(CellData &r_cell_data);
void _navigation_update_cell(CellData &r_cell_data);
#ifdef DEBUG_ENABLED
void _navigation_draw_cell_debug(const RID &p_canvas_item, const Vector2i &p_quadrant_pos, const CellData &r_cell_data);
#endif // DEBUG_ENABLED
HashSet<Vector2i> instantiated_scenes;
void _scenes_update_dirty_quadrants(SelfList<TileMapQuadrant>::List &r_dirty_quadrant_list);
void _scenes_cleanup_quadrant(TileMapQuadrant *p_quadrant);
void _scenes_draw_quadrant_debug(TileMapQuadrant *p_quadrant);
// Runtime tile data.
void _build_runtime_update_tile_data(SelfList<TileMapQuadrant>::List &r_dirty_quadrant_list);
bool _scenes_was_cleaned_up = false;
void _scenes_update();
void _scenes_clear_cell(CellData &r_cell_data);
void _scenes_update_cell(CellData &r_cell_data);
#ifdef DEBUG_ENABLED
void _scenes_draw_cell_debug(const RID &p_canvas_item, const Vector2i &p_quadrant_pos, const CellData &r_cell_data);
#endif // DEBUG_ENABLED
// Terrains.
TileSet::TerrainsPattern _get_best_terrain_pattern_for_constraints(int p_terrain_set, const Vector2i &p_position, const RBSet<TerrainConstraint> &p_constraints, TileSet::TerrainsPattern p_current_pattern);
@ -251,23 +364,10 @@ public:
int get_effective_quadrant_size() const;
// For TileMap node's use.
void notify_canvas_entered();
void notify_visibility_changed();
void notify_xform_changed();
void notify_local_xform_changed();
void notify_canvas_exited();
void notify_selected_layer_changed();
void notify_light_mask_changed();
void notify_material_changed();
void notify_use_parent_material_changed();
void notify_texture_filter_changed();
void notify_texture_repeat_changed();
void update_dirty_quadrants();
void set_tile_data(DataFormat p_format, const Vector<int> &p_data);
Vector<int> get_tile_data() const;
void clear_instantiated_scenes();
void clear_internals(); // Exposed for now to tilemap, but ideally, we should avoid it.
void recreate_internals(); // Exposed for now to tilemap, but ideally, we should avoid it.
void notify_tile_map_change(DirtyFlags p_what);
void internal_update();
// --- Exposed in TileMap ---
@ -310,15 +410,14 @@ public:
void set_navigation_map(RID p_map);
RID get_navigation_map() const;
// In case something goes wrong.
void force_update();
// Fixing and clearing methods.
void fix_invalid_tiles();
// Find coords for body.
bool has_body_rid(RID p_physics_body) const;
Vector2i get_coords_for_body_rid(RID p_physics_body) const; // For finding tiles from collision.
~TileMapLayer();
};
class TileMap : public Node2D {
@ -341,7 +440,7 @@ private:
// Properties.
Ref<TileSet> tile_set;
int quadrant_size = 16;
int rendering_quadrant_size = 16;
bool collision_animatable = false;
VisibilityMode collision_visibility_mode = VISIBILITY_MODE_DEFAULT;
VisibilityMode navigation_visibility_mode = VISIBILITY_MODE_DEFAULT;
@ -349,18 +448,12 @@ private:
// Layers.
LocalVector<Ref<TileMapLayer>> layers;
int selected_layer = -1;
void _clear_internals();
void _recreate_internals();
bool pending_update = false;
Transform2D last_valid_transform;
Transform2D new_transform;
void _tile_set_changed();
bool _tile_set_changed_deferred_update_needed = false;
void _tile_set_changed_deferred_update();
protected:
bool _set(const StringName &p_name, const Variant &p_value);
@ -372,6 +465,9 @@ protected:
#ifndef DISABLE_DEPRECATED
Rect2i _get_used_rect_bind_compat_78328();
void _set_quadrant_size_compat_81070(int p_quadrant_size);
int _get_quadrant_size_compat_81070() const;
static void _bind_compatibility_methods();
#endif
@ -382,15 +478,19 @@ public:
virtual Rect2 _edit_get_rect() const override;
#endif
#ifndef DISABLE_DEPRECATED
void force_update(int p_layer);
#endif
// Called by TileMapLayers.
void queue_update_dirty_quadrants();
void _update_dirty_quadrants();
void queue_internal_update();
void _internal_update();
void set_tileset(const Ref<TileSet> &p_tileset);
Ref<TileSet> get_tileset() const;
void set_quadrant_size(int p_size);
int get_quadrant_size() const;
void set_rendering_quadrant_size(int p_size);
int get_rendering_quadrant_size() const;
static void draw_tile(RID p_canvas_item, const Vector2 &p_position, const Ref<TileSet> p_tile_set, int p_atlas_source_id, const Vector2i &p_atlas_coords, int p_alternative_tile, int p_frame = -1, Color p_modulation = Color(1.0, 1.0, 1.0, 1.0), const TileData *p_tile_data_override = nullptr, real_t p_animation_offset = 0.0);
@ -412,6 +512,7 @@ public:
int get_layer_y_sort_origin(int p_layer) const;
void set_layer_z_index(int p_layer, int p_z_index);
int get_layer_z_index(int p_layer) const;
void set_layer_navigation_map(int p_layer, RID p_map);
RID get_layer_navigation_map(int p_layer) const;
@ -488,7 +589,8 @@ public:
void clear();
// Force a TileMap update.
void force_update(int p_layer = -1);
void update_internals();
void notify_runtime_tile_data_update(int p_layer = -1);
// Helpers?
TypedArray<Vector2i> get_surrounding_cells(const Vector2i &coords);

View file

@ -50,7 +50,6 @@
#endif
class TileMap;
struct TileMapQuadrant;
class TileSetSource;
class TileSetAtlasSource;
class TileData;