make autotiles fall back to the most similar bitmask using heuristics
This commit is contained in:
parent
b3396b093b
commit
73ad6517e4
4 changed files with 168 additions and 21 deletions
|
@ -66,6 +66,13 @@
|
|||
Returns the [enum BitmaskMode] of the autotile.
|
||||
</description>
|
||||
</method>
|
||||
<method name="autotile_get_fallback_mode" qualifiers="const">
|
||||
<return type="int" enum="TileSet.FallbackMode" />
|
||||
<argument index="0" name="id" type="int" />
|
||||
<description>
|
||||
Returns the [enum FallbackMode] of the autotile.
|
||||
</description>
|
||||
</method>
|
||||
<method name="autotile_get_icon_coordinate" qualifiers="const">
|
||||
<return type="Vector2" />
|
||||
<argument index="0" name="id" type="int" />
|
||||
|
@ -139,13 +146,21 @@
|
|||
Sets the [enum BitmaskMode] of the autotile.
|
||||
</description>
|
||||
</method>
|
||||
<method name="autotile_set_fallback_mode">
|
||||
<return type="void" />
|
||||
<argument index="0" name="id" type="int" />
|
||||
<argument index="1" name="mode" type="int" enum="TileSet.FallbackMode" />
|
||||
<description>
|
||||
Returns the [enum FallbackMode] of the autotile.
|
||||
</description>
|
||||
</method>
|
||||
<method name="autotile_set_icon_coordinate">
|
||||
<return type="void" />
|
||||
<argument index="0" name="id" type="int" />
|
||||
<argument index="1" name="coord" type="Vector2" />
|
||||
<description>
|
||||
Sets the subtile that will be used as an icon in an atlas/autotile given its coordinates.
|
||||
The subtile defined as the icon will 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.
|
||||
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.
|
||||
</description>
|
||||
</method>
|
||||
<method name="autotile_set_light_occluder">
|
||||
|
@ -571,6 +586,10 @@
|
|||
</constant>
|
||||
<constant name="BITMASK_3X3" value="2" enum="BitmaskMode">
|
||||
</constant>
|
||||
<constant name="FALLBACK_AUTO" value="0" enum="FallbackMode">
|
||||
</constant>
|
||||
<constant name="FALLBACK_ICON" value="1" enum="FallbackMode">
|
||||
</constant>
|
||||
<constant name="BIND_TOPLEFT" value="1" enum="AutotileBindings">
|
||||
</constant>
|
||||
<constant name="BIND_TOP" value="2" enum="AutotileBindings">
|
||||
|
|
|
@ -2423,6 +2423,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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3450,6 +3451,8 @@ bool TilesetEditorContext::_set(const StringName &p_name, const Variant &p_value
|
|||
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);
|
||||
}
|
||||
|
@ -3516,6 +3519,8 @@ bool TilesetEditorContext::_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);
|
||||
}
|
||||
|
@ -3575,6 +3580,7 @@ void TilesetEditorContext::_get_property_list(List<PropertyInfo> *p_list) const
|
|||
p_list->push_back(PropertyInfo(Variant::INT, PNAME("tile_autotile_bitmask_mode"), PROPERTY_HINT_ENUM, "2x2,3x3 (minimal),3x3"));
|
||||
p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("tile_subtile_size")));
|
||||
p_list->push_back(PropertyInfo(Variant::INT, PNAME("tile_subtile_spacing"), PROPERTY_HINT_RANGE, "0, 1024, 1"));
|
||||
p_list->push_back(PropertyInfo(Variant::INT, PNAME("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, PNAME("tile_subtile_size")));
|
||||
p_list->push_back(PropertyInfo(Variant::INT, PNAME("tile_subtile_spacing"), PROPERTY_HINT_RANGE, "0, 1024, 1"));
|
||||
|
|
|
@ -148,6 +148,8 @@ bool TileSet::_set(const StringName &p_name, const Variant &p_value) {
|
|||
}
|
||||
p.pop_front();
|
||||
}
|
||||
} else if (what == "fallback_mode") {
|
||||
autotile_set_fallback_mode(id, (FallbackMode)((int)p_value));
|
||||
}
|
||||
} else if (what == "shape") {
|
||||
if (tile_get_shape_count(id) > 0) {
|
||||
|
@ -293,6 +295,8 @@ bool TileSet::_get(const StringName &p_name, Variant &r_ret) const {
|
|||
}
|
||||
}
|
||||
r_ret = p;
|
||||
} else if (what == "fallback_mode") {
|
||||
r_ret = autotile_get_fallback_mode(id);
|
||||
}
|
||||
} else if (what == "shape") {
|
||||
r_ret = tile_get_shape(id, 0);
|
||||
|
@ -345,6 +349,7 @@ void TileSet::_get_property_list(List<PropertyInfo> *p_list) const {
|
|||
p_list->push_back(PropertyInfo(Variant::ARRAY, pre + "autotile/navpoly_map", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
|
||||
p_list->push_back(PropertyInfo(Variant::ARRAY, pre + "autotile/priority_map", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
|
||||
p_list->push_back(PropertyInfo(Variant::ARRAY, pre + "autotile/z_index_map", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
|
||||
p_list->push_back(PropertyInfo(Variant::INT, pre + "autotile/fallback_mode", PROPERTY_HINT_ENUM, "Auto,Icon", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
|
||||
} else if (tile_get_tile_mode(id) == ATLAS_TILE) {
|
||||
p_list->push_back(PropertyInfo(Variant::VECTOR2, pre + "autotile/icon_coordinate", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
|
||||
p_list->push_back(PropertyInfo(Variant::VECTOR2, pre + "autotile/tile_size", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
|
||||
|
@ -550,6 +555,18 @@ const Map<Vector2, int> &TileSet::autotile_get_z_index_map(int p_id) const {
|
|||
return tile_map[p_id].autotile_data.z_index_map;
|
||||
}
|
||||
|
||||
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(int p_id, const Vector2 &p_coord, uint32_t p_flag) {
|
||||
ERR_FAIL_COND_MSG(!tile_map.has(p_id), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id));
|
||||
if (p_flag == 0) {
|
||||
|
@ -589,23 +606,8 @@ const Map<Vector2, uint32_t> &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<Vector2> TileSet::_autotile_get_subtile_candidates_for_bitmask(int p_id, uint16_t p_bitmask) const {
|
||||
List<Vector2> coords;
|
||||
List<uint32_t> priorities;
|
||||
uint32_t priority_sum = 0;
|
||||
uint32_t mask;
|
||||
uint16_t mask_;
|
||||
uint16_t mask_ignore;
|
||||
|
@ -619,16 +621,119 @@ 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<Vector2> 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 (Map<Vector2, uint32_t>::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<uint32_t> priorities;
|
||||
uint32_t priority_sum = 0;
|
||||
for (List<Vector2>::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;
|
||||
|
@ -1123,6 +1228,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);
|
||||
|
@ -1178,6 +1285,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);
|
||||
|
|
|
@ -90,6 +90,11 @@ public:
|
|||
ATLAS_TILE
|
||||
};
|
||||
|
||||
enum FallbackMode {
|
||||
FALLBACK_AUTO,
|
||||
FALLBACK_ICON
|
||||
};
|
||||
|
||||
struct AutotileData {
|
||||
BitmaskMode bitmask_mode;
|
||||
Size2 size;
|
||||
|
@ -100,13 +105,15 @@ public:
|
|||
Map<Vector2, Ref<NavigationPolygon>> navpoly_map;
|
||||
Map<Vector2, int> priority_map;
|
||||
Map<Vector2, int> 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:
|
||||
|
@ -144,6 +151,7 @@ protected:
|
|||
Array _tile_get_shapes(int p_id) const;
|
||||
Array _get_tiles_ids() const;
|
||||
void _decompose_convex_shape(Ref<Shape2D> p_shape);
|
||||
List<Vector2> _autotile_get_subtile_candidates_for_bitmask(int p_id, uint16_t p_bitmask) const;
|
||||
|
||||
static void _bind_methods();
|
||||
|
||||
|
@ -189,6 +197,9 @@ public:
|
|||
int autotile_get_z_index(int p_id, const Vector2 &p_coord);
|
||||
const Map<Vector2, int> &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 Map<Vector2, uint32_t> &autotile_get_bitmask_map(int p_id);
|
||||
|
@ -265,5 +276,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
|
||||
|
|
Loading…
Reference in a new issue