diff --git a/modules/tile_map/doc_classes/TileSet.xml b/modules/tile_map/doc_classes/TileSet.xml
index 7f21c1bee..a1e5690f7 100644
--- a/modules/tile_map/doc_classes/TileSet.xml
+++ b/modules/tile_map/doc_classes/TileSet.xml
@@ -50,6 +50,13 @@
+
+
+
+
+ Returns the [enum FallbackMode] of the autotile.
+
+
@@ -111,11 +118,20 @@
+
+
+
+
+
+ Returns the [enum FallbackMode] of the autotile.
+
+
+ The subtile defined as the icon may be used as a fallback when the atlas/autotile's bitmask information is incomplete. It will also be used to represent it in the TileSet editor.
@@ -488,6 +504,10 @@
+
+
+
+
diff --git a/modules/tile_map/tile_set.cpp b/modules/tile_map/tile_set.cpp
index c0944992a..e4159fc3e 100644
--- a/modules/tile_map/tile_set.cpp
+++ b/modules/tile_map/tile_set.cpp
@@ -29,8 +29,8 @@
/*************************************************************************/
#include "tile_set.h"
-#include "core/variant/array.h"
#include "core/config/engine.h"
+#include "core/variant/array.h"
bool TileSet::_set(const StringName &p_name, const Variant &p_value) {
String n = p_name;
@@ -376,6 +376,18 @@ void TileSet::create_tile(int p_id) {
emit_changed();
}
+void TileSet::autotile_set_fallback_mode(int p_id, FallbackMode p_mode) {
+ ERR_FAIL_COND_MSG(!tile_map.has(p_id), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id));
+ tile_map[p_id].autotile_data.fallback_mode = p_mode;
+ _change_notify("");
+ emit_changed();
+}
+
+TileSet::FallbackMode TileSet::autotile_get_fallback_mode(int p_id) const {
+ ERR_FAIL_COND_V_MSG(!tile_map.has(p_id), FALLBACK_AUTO, vformat("The TileSet doesn't have a tile with ID '%d'.", p_id));
+ return tile_map[p_id].autotile_data.fallback_mode;
+}
+
void TileSet::autotile_set_bitmask_mode(int p_id, BitmaskMode p_mode) {
ERR_FAIL_COND_MSG(!tile_map.has(p_id), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id));
tile_map[p_id].autotile_data.bitmask_mode = p_mode;
@@ -589,23 +601,8 @@ const RBMap &TileSet::autotile_get_bitmask_map(int p_id) {
}
}
-Vector2 TileSet::autotile_get_subtile_for_bitmask(int p_id, uint16_t p_bitmask, const Node *p_tilemap_node, const Vector2 &p_tile_location) {
- ERR_FAIL_COND_V_MSG(!tile_map.has(p_id), Vector2(), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id));
- //First try to forward selection to script
- if (p_tilemap_node->get_class_name() == "TileMap") {
- if (get_script_instance() != nullptr) {
- if (get_script_instance()->has_method("_forward_subtile_selection")) {
- Variant ret = get_script_instance()->call("_forward_subtile_selection", p_id, p_bitmask, p_tilemap_node, p_tile_location);
- if (ret.get_type() == Variant::VECTOR2) {
- return ret;
- }
- }
- }
- }
-
+List TileSet::_autotile_get_subtile_candidates_for_bitmask(int p_id, uint16_t p_bitmask) const {
List coords;
- List priorities;
- uint32_t priority_sum = 0;
uint32_t mask;
uint16_t mask_;
uint16_t mask_ignore;
@@ -619,16 +616,120 @@ Vector2 TileSet::autotile_get_subtile_for_bitmask(int p_id, uint16_t p_bitmask,
mask_ignore = mask >> 16;
if (((mask_ & (~mask_ignore)) == (p_bitmask & (~mask_ignore))) && (((~mask_) | mask_ignore) == ((~p_bitmask) | mask_ignore))) {
- uint32_t priority = autotile_get_subtile_priority(p_id, E->key());
- priority_sum += priority;
- priorities.push_back(priority);
coords.push_back(E->key());
}
}
+ return coords;
+}
+
+uint32_t _count_bitmask_bits(uint32_t bitmask) {
+ uint32_t ret = 0;
+
+ for (uint32_t i = 1; i <= 256; i <<= 1) {
+ if (bitmask & i) {
+ ret++;
+ }
+ }
+
+ return ret;
+}
+uint32_t _score_bitmask_difference(uint32_t bitmask, uint32_t ref_bitmask) {
+ // low = less difference, high = more difference
+ uint32_t ret = 0;
+
+ bitmask ^= ref_bitmask;
+ // add one to the score for each non-matching bit
+ for (uint32_t i = 1; i <= 256; i <<= 1) {
+ if (bitmask & i) {
+ ret += 1;
+ // make axial edge mismatches cost four times as much
+ if (i & (TileSet::BIND_TOP | TileSet::BIND_LEFT | TileSet::BIND_RIGHT | TileSet::BIND_BOTTOM)) {
+ ret += 3;
+ }
+ }
+ }
+ bitmask ^= ref_bitmask;
+ // artificially reduce difference for all-filled and all-but-center-empty bitmasks
+ // (511 is the non-IGNORE bitmasks all or'd together; 0x1FF)
+ if (ret > 0 && (bitmask == 511 || bitmask == TileSet::BIND_CENTER)) {
+ ret -= 1;
+ }
+ // artificially increase difference for non-symmetric bitmasks if testing against all-filled or all-but-center-empty bitmask
+ // (need to cast the bit tests from int to bool before comparing)
+ if ((ref_bitmask == 511 || ref_bitmask == TileSet::BIND_CENTER) &&
+ (bool(bitmask & TileSet::BIND_LEFT) != bool(bitmask & TileSet::BIND_RIGHT) ||
+ bool(bitmask & TileSet::BIND_TOP) != bool(bitmask & TileSet::BIND_BOTTOM) ||
+ bool(bitmask & TileSet::BIND_TOPRIGHT) != bool(bitmask & TileSet::BIND_BOTTOMLEFT) ||
+ bool(bitmask & TileSet::BIND_TOPLEFT) != bool(bitmask & TileSet::BIND_BOTTOMRIGHT))) {
+ ret += 16;
+ }
+ return ret;
+}
+
+Vector2 TileSet::autotile_get_subtile_for_bitmask(int p_id, uint16_t p_bitmask, const Node *p_tilemap_node, const Vector2 &p_tile_location) {
+ ERR_FAIL_COND_V_MSG(!tile_map.has(p_id), Vector2(), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id));
+ // First try to forward selection to script
+ if (p_tilemap_node->get_class_name() == "TileMap") {
+ if (get_script_instance() != nullptr) {
+ if (get_script_instance()->has_method("_forward_subtile_selection")) {
+ Variant ret = get_script_instance()->call("_forward_subtile_selection", p_id, p_bitmask, p_tilemap_node, p_tile_location);
+ if (ret.get_type() == Variant::VECTOR2) {
+ return ret;
+ }
+ }
+ }
+ }
+
+ // if no forward-selected tile, look for a matching tile
+ List coords = _autotile_get_subtile_candidates_for_bitmask(p_id, p_bitmask);
+
+ // if we didn't find anything, and auto fallback is enagled, try falling back to a tile with a similar bitmask instead of the default tile
+ if (tile_map[p_id].autotile_data.fallback_mode == FALLBACK_AUTO && coords.size() == 0) {
+ uint32_t best_match_cost = 100000; // main point of comparison, general difference between bitmasks
+ uint32_t best_match_bitcount = 0; // bit count, as a tie breaker
+ uint16_t best_match_bitmask = 0;
+
+ for (RBMap::Element *E = tile_map[p_id].autotile_data.flags.front(); E; E = E->next()) {
+ uint32_t mask = E->get();
+ if (tile_map[p_id].autotile_data.bitmask_mode == BITMASK_2X2) {
+ mask |= (BIND_IGNORE_TOP | BIND_IGNORE_LEFT | BIND_IGNORE_CENTER | BIND_IGNORE_RIGHT | BIND_IGNORE_BOTTOM);
+ }
+
+ uint16_t mask_ignore = mask >> 16;
+ uint16_t mask_low = mask & 0xFFFF;
+ mask_low &= ~mask_ignore;
+ mask_low |= p_bitmask & mask_ignore;
+
+ // always skip bitmasks with no center bit, or that have already been matched as the best
+ if ((mask_low & BIND_CENTER) == 0 || mask_low == best_match_bitmask) {
+ continue;
+ }
+
+ uint32_t cost = _score_bitmask_difference(mask_low, p_bitmask);
+ uint32_t bitcount = _count_bitmask_bits(mask_low); // to break ties, pick the bitmask with more set bits
+
+ // if more similar, confirm match
+ if (cost < best_match_cost || (cost == best_match_cost && bitcount > best_match_bitcount)) {
+ best_match_cost = cost;
+ best_match_bitcount = bitcount;
+ best_match_bitmask = mask_low;
+ }
+ }
+ coords = _autotile_get_subtile_candidates_for_bitmask(p_id, best_match_bitmask);
+ }
+
if (coords.size() == 0) {
return autotile_get_icon_coordinate(p_id);
} else {
+ List priorities;
+ uint32_t priority_sum = 0;
+ for (List::Element *E = coords.front(); E; E = E->next()) {
+ uint32_t priority = autotile_get_subtile_priority(p_id, E->get());
+ priority_sum += priority;
+ priorities.push_back(priority);
+ }
+
uint32_t picked_value = Math::rand() % priority_sum;
uint32_t upper_bound;
uint32_t lower_bound = 0;
@@ -1135,6 +1236,8 @@ void TileSet::_bind_methods() {
ClassDB::bind_method(D_METHOD("autotile_get_spacing", "id"), &TileSet::autotile_get_spacing);
ClassDB::bind_method(D_METHOD("autotile_set_size", "id", "size"), &TileSet::autotile_set_size);
ClassDB::bind_method(D_METHOD("autotile_get_size", "id"), &TileSet::autotile_get_size);
+ ClassDB::bind_method(D_METHOD("autotile_set_fallback_mode", "id", "mode"), &TileSet::autotile_set_fallback_mode);
+ ClassDB::bind_method(D_METHOD("autotile_get_fallback_mode", "id"), &TileSet::autotile_get_fallback_mode);
ClassDB::bind_method(D_METHOD("tile_set_name", "id", "name"), &TileSet::tile_set_name);
ClassDB::bind_method(D_METHOD("tile_get_name", "id"), &TileSet::tile_get_name);
ClassDB::bind_method(D_METHOD("tile_set_texture", "id", "texture"), &TileSet::tile_set_texture);
@@ -1196,6 +1299,9 @@ void TileSet::_bind_methods() {
BIND_ENUM_CONSTANT(BITMASK_3X3_MINIMAL);
BIND_ENUM_CONSTANT(BITMASK_3X3);
+ BIND_ENUM_CONSTANT(FALLBACK_AUTO);
+ BIND_ENUM_CONSTANT(FALLBACK_ICON);
+
BIND_ENUM_CONSTANT(BIND_TOPLEFT);
BIND_ENUM_CONSTANT(BIND_TOP);
BIND_ENUM_CONSTANT(BIND_TOPRIGHT);
diff --git a/modules/tile_map/tile_set.h b/modules/tile_map/tile_set.h
index 6317d09e3..1923e6b8b 100644
--- a/modules/tile_map/tile_set.h
+++ b/modules/tile_map/tile_set.h
@@ -32,12 +32,12 @@
#include "../fastnoise/fastnoise_noise_params.h"
#include "../fastnoise/noise.h"
-#include "core/variant/array.h"
#include "core/object/resource.h"
+#include "core/variant/array.h"
#include "scene/2d/light_occluder_2d.h"
#include "scene/2d/navigation_polygon_instance.h"
-#include "scene/resources/navigation_polygon.h"
#include "scene/resources/convex_polygon_shape_2d.h"
+#include "scene/resources/navigation_polygon.h"
#include "scene/resources/shape_2d.h"
#include "scene/resources/texture.h"
@@ -92,6 +92,11 @@ public:
ATLAS_TILE
};
+ enum FallbackMode {
+ FALLBACK_AUTO,
+ FALLBACK_ICON
+ };
+
struct AutotileData {
BitmaskMode bitmask_mode;
Size2 size;
@@ -102,13 +107,15 @@ public:
RBMap> navpoly_map;
RBMap priority_map;
RBMap z_index_map;
+ FallbackMode fallback_mode;
// Default size to prevent invalid value
explicit AutotileData() :
bitmask_mode(BITMASK_2X2),
size(64, 64),
spacing(0),
- icon_coord(0, 0) {}
+ icon_coord(0, 0),
+ fallback_mode(FALLBACK_AUTO) {}
};
private:
@@ -148,6 +155,7 @@ protected:
Array _tile_get_shapes(int p_id) const;
Array _get_tiles_ids() const;
void _decompose_convex_shape(Ref p_shape);
+ List _autotile_get_subtile_candidates_for_bitmask(int p_id, uint16_t p_bitmask) const;
static void _bind_methods();
@@ -193,6 +201,9 @@ public:
int autotile_get_z_index(int p_id, const Vector2 &p_coord);
const RBMap &autotile_get_z_index_map(int p_id) const;
+ void autotile_set_fallback_mode(int p_id, FallbackMode p_mode);
+ FallbackMode autotile_get_fallback_mode(int p_id) const;
+
void autotile_set_bitmask(int p_id, const Vector2 &p_coord, uint32_t p_flag);
uint32_t autotile_get_bitmask(int p_id, const Vector2 &p_coord);
const RBMap &autotile_get_bitmask_map(int p_id);
@@ -273,5 +284,6 @@ public:
VARIANT_ENUM_CAST(TileSet::AutotileBindings);
VARIANT_ENUM_CAST(TileSet::BitmaskMode);
VARIANT_ENUM_CAST(TileSet::TileMode);
+VARIANT_ENUM_CAST(TileSet::FallbackMode);
#endif // TILE_SET_H
diff --git a/modules/tile_map/tile_set_editor_plugin.cpp b/modules/tile_map/tile_set_editor_plugin.cpp
index d38875a24..b1a7c0272 100644
--- a/modules/tile_map/tile_set_editor_plugin.cpp
+++ b/modules/tile_map/tile_set_editor_plugin.cpp
@@ -2454,6 +2454,7 @@ void TileSetEditor::_undo_tile_removal(int p_id) {
undo_redo->add_undo_method(tileset.ptr(), "autotile_set_size", p_id, tileset->autotile_get_size(p_id));
undo_redo->add_undo_method(tileset.ptr(), "autotile_set_spacing", p_id, tileset->autotile_get_spacing(p_id));
undo_redo->add_undo_method(tileset.ptr(), "autotile_set_bitmask_mode", p_id, tileset->autotile_get_bitmask_mode(p_id));
+ undo_redo->add_undo_method(tileset.ptr(), "autotile_set_fallback_mode", p_id, tileset->autotile_get_fallback_mode(p_id));
}
}
@@ -3487,6 +3488,8 @@ bool RTilesetEditorContext::_set(const StringName &p_name, const Variant &p_valu
tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/tile_size", p_value, &v);
} else if (name2 == "subtile_spacing") {
tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/spacing", p_value, &v);
+ } else if (name2 == "autotile_fallback_mode") {
+ tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/fallback_mode", p_value, &v);
} else {
tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/" + name2, p_value, &v);
}
@@ -3553,6 +3556,8 @@ bool RTilesetEditorContext::_get(const StringName &p_name, Variant &r_ret) const
r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/tile_size", &v);
} else if (name == "subtile_spacing") {
r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/spacing", &v);
+ } else if (name == "autotile_fallback_mode") {
+ r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/fallback_mode", &v);
} else {
r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/" + name, &v);
}
@@ -3612,6 +3617,7 @@ void RTilesetEditorContext::_get_property_list(List *p_list) const
p_list->push_back(PropertyInfo(Variant::INT, "tile_autotile_bitmask_mode", PROPERTY_HINT_ENUM, "2x2,3x3 (minimal),3x3"));
p_list->push_back(PropertyInfo(Variant::VECTOR2, "tile_subtile_size"));
p_list->push_back(PropertyInfo(Variant::INT, "tile_subtile_spacing", PROPERTY_HINT_RANGE, "0, 1024, 1"));
+ p_list->push_back(PropertyInfo(Variant::INT, "tile_autotile_fallback_mode", PROPERTY_HINT_ENUM, "Auto,Icon"));
} else if (tileset->tile_get_tile_mode(id) == TileSet::ATLAS_TILE) {
p_list->push_back(PropertyInfo(Variant::VECTOR2, "tile_subtile_size"));
p_list->push_back(PropertyInfo(Variant::INT, "tile_subtile_spacing", PROPERTY_HINT_RANGE, "0, 1024, 1"));