From 7eb4c5de752f5b4d2825f03f3edafe33ba2e96da Mon Sep 17 00:00:00 2001 From: raffimolero Date: Thu, 26 Dec 2024 15:02:04 +0800 Subject: [PATCH 1/5] document TileMapDual --- addons/TileMapDual/TileMapDual.gd | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/TileMapDual/TileMapDual.gd b/addons/TileMapDual/TileMapDual.gd index fbfc7f0..62b33a5 100644 --- a/addons/TileMapDual/TileMapDual.gd +++ b/addons/TileMapDual/TileMapDual.gd @@ -24,9 +24,12 @@ func _atlas_added(source_id: int, atlas: TileSetAtlasSource): pass -## Sets up the Dual-Grid illusion. +## Makes the main world grid invisible. +## The main tiles don't need to be seen. Only the DisplayLayers should be visible. ## Called on ready. func _make_self_invisible() -> void: + if material != null: + return material = CanvasItemMaterial.new() material.light_mode = CanvasItemMaterial.LightMode.LIGHT_MODE_LIGHT_ONLY @@ -56,7 +59,7 @@ func _changed() -> void: ## 'terrain' is which terrain type to draw. ## terrain -1 completely removes the tile, ## and by default, terrain 0 is the empty tile. -func draw(cell: Vector2i, terrain: int = 1) -> void: +func draw_cell(cell: Vector2i, terrain: int = 1) -> void: var terrains := _display.terrain.terrains if terrain not in terrains: erase_cell(cell) From 1557aec0cad3b2b374e02789d6291c3cafbd07ec Mon Sep 17 00:00:00 2001 From: raffimolero Date: Thu, 26 Dec 2024 15:02:33 +0800 Subject: [PATCH 2/5] document TileSetWatcher --- addons/TileMapDual/TileSetWatcher.gd | 59 +++++++++++++++++++++------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/addons/TileMapDual/TileSetWatcher.gd b/addons/TileMapDual/TileSetWatcher.gd index 9a70f7a..ca0233f 100644 --- a/addons/TileMapDual/TileSetWatcher.gd +++ b/addons/TileMapDual/TileSetWatcher.gd @@ -1,9 +1,15 @@ +## Provides information about a TileSet and sends signals when it changes. class_name TileSetWatcher extends Resource +## Caches the previous tile_set to see when it changes. +var tile_set: TileSet +## Caches the previous tile_size to see when it changes. var tile_size: Vector2i +## Caches the previous result of Display.tileset_grid_shape(tile_set) to see when it changes. var grid_shape: Display.GridShape func _init(tile_set: TileSet) -> void: + # TODO: inline all functions here except atlas added tileset_deleted.connect(_tileset_deleted, 1) tileset_created.connect(_tileset_created, 1) tileset_resized.connect(_tileset_resized, 1) @@ -14,57 +20,63 @@ func _init(tile_set: TileSet) -> void: var _flag_tileset_deleted := false +## Emitted when the parent TileMapDual's tile_set is cleared or replaced. signal tileset_deleted func _tileset_deleted(): #print('SIGNAL EMITTED: tileset_deleted(%s)' % {}) - #tileset_reshaped.emit() pass var _flag_tileset_created := false +## Emitted when the parent TileMapDual's tile_set is created or replaced. signal tileset_created func _tileset_created(): #print('SIGNAL EMITTED: tileset_created(%s)' % {}) - #tileset_reshaped.emit() pass var _flag_tileset_resized := false +## Emitted when tile_set.tile_size is changed. signal tileset_resized func _tileset_resized(): #print('SIGNAL EMITTED: tileset_resized(%s)' % {}) pass var _flag_tileset_reshaped := false +## Emitted when the GridShape of the TileSet would be different. signal tileset_reshaped func _tileset_reshaped(): #print('SIGNAL EMITTED: tileset_reshaped(%s)' % {}) - #terrains_changed.emit() pass var _flag_atlas_added := false +## Emitted when a new Atlas is added to this TileSet. +## Does not react to Scenes being added to the TileSet. signal atlas_added(source_id: int, atlas: TileSetAtlasSource) func _atlas_added(source_id: int, atlas: TileSetAtlasSource): _flag_atlas_added = true #print('SIGNAL EMITTED: atlas_added(%s)' % {'source_id': source_id, 'atlas': atlas}) - #terrains_changed.emit() pass var _flag_terrains_changed := false +## Emitted when an atlas is added or removed, +## or when the terrains change in one of the Atlases. signal terrains_changed func _terrains_changed(): #print('SIGNAL EMITTED: terrains_changed(%s)' % {}) pass +## Checks if anything about the concerned TileMapDual's tile_set changed. +## Must be called by the TileMapDual every frame. func update(tile_set: TileSet) -> void: check_tile_set(tile_set) check_flags() -## Emit updates if the corresponding flags were set. +## Emit update signals if the corresponding flags were set. ## Must only be run once per frame. func check_flags() -> void: if _flag_tileset_changed: - _update_tileset() + _check_tileset() if _flag_tileset_deleted: _flag_tileset_deleted = false _flag_tileset_reshaped = true @@ -88,7 +100,6 @@ func check_flags() -> void: terrains_changed.emit() -var tile_set: TileSet ## Check if tile_set has been added, replaced, or deleted. func check_tile_set(tile_set: TileSet) -> void: if tile_set == self.tile_set: @@ -107,11 +118,15 @@ func check_tile_set(tile_set: TileSet) -> void: var _flag_tileset_changed := false +## Helper method to be called when the tile_set detects a change. +## Must be disconnected when the tile_set is changed. func _set_tileset_changed() -> void: _flag_tileset_changed = true -func _update_tileset() -> void: +## Called when _flag_tileset_changed. +## Provides more detail about what changed. +func _check_tileset() -> void: var tile_size = tile_set.tile_size if self.tile_size != tile_size: self.tile_size = tile_size @@ -120,18 +135,23 @@ func _update_tileset() -> void: if self.grid_shape != grid_shape: self.grid_shape = grid_shape _flag_tileset_reshaped = true - _update_tileset_atlases() + _check_tileset_atlases() -## Configures all tile set atlases -# TODO: detect automatic tile creation +# Cached variables from the previous frame +# These are used to compare what changed between frames var _cached_source_count: int = 0 var _cached_sids := Set.new() -func _update_tileset_atlases(): +# TODO: detect automatic tile creation +## Checks if new atlases have been added. +## Does not check which ones were deleted. +func _check_tileset_atlases(): # Update all tileset sources var source_count := tile_set.get_source_count() var terrain_set_count := tile_set.get_terrain_sets_count() + # Only if an asset was added or removed + # FIXME?: may break on add+remove in 1 frame if _cached_source_count == source_count: return _cached_source_count = source_count @@ -141,7 +161,6 @@ func _update_tileset_atlases(): for i in source_count: var sid: int = tile_set.get_source_id(i) sids.insert(sid) - #print('checking') if _cached_sids.has(sid): continue var source: TileSetSource = tile_set.get_source(sid) @@ -153,7 +172,19 @@ func _update_tileset_atlases(): continue var atlas: TileSetAtlasSource = source atlas_added.emit(sid, atlas) + # FIXME?: check if this needs to be disconnected + # SETUP: + # - add logging to check which Watcher's flag was changed + # - add a TileSet with an atlas to 2 TileMapDuals + # - remove the TileSet + # - modify the terrains on one of the atlases + # - check how many watchers were flagged: + # - if 2 watchers were flagged, this is bad. + # try to repeatedly add and remove the tileset. + # this could either cause the flag to happen multiple times, + # or it could stay at 2 watchers. + # - if 1 watcher was flagged, that is ok atlas.changed.connect(func(): _flag_terrains_changed = true, 1) - #push_error('update atlases') _flag_terrains_changed = true + # FIXME?: find which sid's were deleted _cached_sids = sids From 635a4b703322ae7a768fc25b0d1593aa825dd05c Mon Sep 17 00:00:00 2001 From: raffimolero Date: Thu, 26 Dec 2024 15:46:13 +0800 Subject: [PATCH 3/5] document Display, DisplayLayer, TileCache, CursorDual --- addons/TileMapDual/CursorDual.gd | 8 +- addons/TileMapDual/Display.gd | 146 +++++++++++------------------ addons/TileMapDual/DisplayLayer.gd | 42 ++++++--- addons/TileMapDual/TerrainDual.gd | 28 ++++-- addons/TileMapDual/TileCache.gd | 56 +++++++++++ 5 files changed, 167 insertions(+), 113 deletions(-) create mode 100644 addons/TileMapDual/TileCache.gd diff --git a/addons/TileMapDual/CursorDual.gd b/addons/TileMapDual/CursorDual.gd index d6eee42..b2140cb 100644 --- a/addons/TileMapDual/CursorDual.gd +++ b/addons/TileMapDual/CursorDual.gd @@ -29,14 +29,12 @@ func _process(_delta: float) -> void: if Input.is_action_pressed("quick_action_2"): terrain_offset = 2 # Clicking the 0 key activates tile removal. - # It does remove tiles for both right and left clicks, since atlas_id = -1. - # The same can be achieved with: - # tilemap_dual.remove_tile(cell) + # It does remove tiles for both right and left clicks, since the terrain is -1. if Input.is_action_pressed("quick_action_0"): terrain_offset = -1 cell = tilemap_dual.local_to_map(global_position) if Input.is_action_pressed("left_click"): - tilemap_dual.draw(cell, terrain_offset + 1) + tilemap_dual.draw_cell(cell, terrain_offset + 1) elif Input.is_action_pressed("right_click"): - tilemap_dual.draw(cell, terrain_offset) + tilemap_dual.draw_cell(cell, terrain_offset) diff --git a/addons/TileMapDual/Display.gd b/addons/TileMapDual/Display.gd index 4b55caf..398013a 100644 --- a/addons/TileMapDual/Display.gd +++ b/addons/TileMapDual/Display.gd @@ -1,4 +1,5 @@ -## Manages up to 2 DisplayLayer children. +## A Node designed to hold and manage up to 2 DisplayLayer children. +## See DisplayLayer.gd for details. class_name Display extends Node @@ -6,22 +7,34 @@ extends Node const TODO = null +## See TerrainDual.gd var terrain: TerrainDual +## See TileSetWatcher.gd var _tileset_watcher: TileSetWatcher ## Creates a new Display that updates when the TileSet updates. func _init(tileset_watcher: TileSetWatcher) -> void: #print('initializing Display...') _tileset_watcher = tileset_watcher terrain = TerrainDual.new(tileset_watcher) - terrain.changed.connect(_tileset_reshaped, 1) + terrain.changed.connect(_terrain_changed, 1) world_tiles_changed.connect(_world_tiles_changed, 1) + +## Activates when the TerrainDual changes. +func _terrain_changed(): + _delete_layers() + _create_layers() + + +## Emitted when the tiles in the map have been edited. signal world_tiles_changed(changed: Array) func _world_tiles_changed(changed: Array): #print('SIGNAL EMITTED: world_tiles_changed(%s)' % {'changed': changed}) for child in get_children(true): child.update_tiles(cached_cells, changed) + +## Initializes and configures new DisplayLayers according to the grid shape. func _create_layers(): #print('GRID SHAPE: %s' % _tileset_watcher.grid_shape) var grid: Array = GRIDS[_tileset_watcher.grid_shape] @@ -32,71 +45,20 @@ func _create_layers(): add_child(layer) layer.update_tiles_all(cached_cells) + +## Deletes all of the DisplayLayers. func _delete_layers(): for child in get_children(true): child.queue_free() -func _tileset_reshaped(): - _delete_layers() - _create_layers() - - -class CellCache: - extends Resource - - var cells := {} - func _init() -> void: - pass - ## Computes a new CellCache based on the current layer data. - ## Needs the old CellCache in case corrections need to made due to accidents. - func compute(tile_set: TileSet, layer: TileMapLayer, cache: CellCache) -> void: - if tile_set == null: - push_error('Attempted to construct CellCache while tile set was null') - return - for cell in layer.get_used_cells(): - # Invalid cells will be treated as empty and ignored - var sid := layer.get_cell_source_id(cell) - if not tile_set.has_source(sid): - continue - var src = tile_set.get_source(sid) - var tile := layer.get_cell_atlas_coords(cell) - if not src.has_tile(tile): - continue - var data := layer.get_cell_tile_data(cell) - if data == null: - continue - # Accidental cells should be reset to their previous value - # They will be treated as unchanged - if data.terrain == -1 or data.terrain_set != 0: - if cell not in cache.cells: - layer.erase_cell(cell) - continue - var cached: Dictionary = cache.cells[cell] - sid = cached.sid - tile = cached.tile - layer.set_cell(cell, cached.sid, cached.tile) - cells[cell] = {'sid': sid, 'tile': tile, 'terrain': data.terrain} - - ## Returns the difference between two tile caches - func diff(other: CellCache) -> Array[Vector2i]: - var out: Array[Vector2i] = [] - for key in cells: - if key not in other.cells or cells[key].terrain != other.cells[key].terrain: - out.push_back(key) - for key in other.cells: - if key not in cells: - out.push_back(key) - return out - - -## {Vector2i: {'sid': int, 'tile': Vector2i}} -var cached_cells := CellCache.new() -## Updates the display based on the cells found in the TileMapLayer. +## The TileCache computed from the last time update() was called. +var cached_cells := TileCache.new() +## Updates the display based on the cells changed in the TileMapLayer. func update(layer: TileMapLayer): if _tileset_watcher.tile_set == null: return - var current := CellCache.new() + var current := TileCache.new() current.compute(_tileset_watcher.tile_set, layer, cached_cells) var updated := current.diff(cached_cells) cached_cells = current @@ -105,8 +67,9 @@ func update(layer: TileMapLayer): world_tiles_changed.emit(updated) +# TODO: phase out GridShape and simply transpose everything when the offset axis is vertical ## Returns what kind of grid a TileSet is. -## Defaults to SQUARE. +## Will default to SQUARE if Godot decides to add a new TileShape. static func tileset_gridshape(tile_set: TileSet) -> GridShape: var hori: bool = tile_set.tile_offset_axis == TileSet.TILE_OFFSET_AXIS_HORIZONTAL match tile_set.tile_shape: @@ -122,7 +85,7 @@ static func tileset_gridshape(tile_set: TileSet) -> GridShape: return GridShape.SQUARE -## Every meningfully different TileSet.tile_shape * TileSet.tile_offset_axis combination. +## Every meaningfully different TileSet.tile_shape * TileSet.tile_offset_axis combination. enum GridShape { SQUARE, ISO, @@ -134,34 +97,35 @@ enum GridShape { ## How to deal with every available GridShape. +## See DisplayLayer.gd for more information about these fields. const GRIDS: Dictionary = { GridShape.SQUARE: [ { # [] 'offset': Vector2(-0.5, -0.5), - 'dual_to_display': [ - [], - [TileSet.CELL_NEIGHBOR_RIGHT_SIDE], - [TileSet.CELL_NEIGHBOR_BOTTOM_SIDE], - [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER], - ], - 'display_to_dual': [ - [TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER], - [TileSet.CELL_NEIGHBOR_TOP_SIDE], - [TileSet.CELL_NEIGHBOR_LEFT_SIDE], - [], + 'world_to_affected_display_neighbors': [ + [], # NW + [TileSet.CELL_NEIGHBOR_RIGHT_SIDE], # NE + [TileSet.CELL_NEIGHBOR_BOTTOM_SIDE], # SW + [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER], # SE + ], + 'display_to_world_neighbors': [ + [TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER], # NW + [TileSet.CELL_NEIGHBOR_TOP_SIDE], # NE + [TileSet.CELL_NEIGHBOR_LEFT_SIDE], # SW + [], # SE ], } ], GridShape.ISO: [ { # <> 'offset': Vector2(0, -0.5), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], # TOP [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], # RIGHT [TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE], # LEFT [TileSet.CELL_NEIGHBOR_BOTTOM_CORNER], # BOTTOM ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [TileSet.CELL_NEIGHBOR_TOP_CORNER], # TOP [TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE], # RIGHT [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], # LEFT @@ -172,24 +136,24 @@ const GRIDS: Dictionary = { GridShape.HALF_OFF_HORI: [ { 'offset': Vector2(0.0, -0.5), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [], [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE], ], }, { 'offset': Vector2(-0.5, -0.5), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], [TileSet.CELL_NEIGHBOR_RIGHT_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_LEFT_SIDE], [], @@ -199,24 +163,24 @@ const GRIDS: Dictionary = { GridShape.HALF_OFF_VERT: [ { 'offset': Vector2(-0.5, 0.0), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [], [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE], ], }, { 'offset': Vector2(-0.5, -0.5), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_TOP_SIDE], [], @@ -226,24 +190,24 @@ const GRIDS: Dictionary = { GridShape.HEX_HORI: [ { 'offset': Vector2(0.0, -3.0 / 8.0), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [], [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE], ], }, { 'offset': Vector2(-0.5, -3.0 / 8.0), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], [TileSet.CELL_NEIGHBOR_RIGHT_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_LEFT_SIDE], [], @@ -251,26 +215,26 @@ const GRIDS: Dictionary = { }, ], GridShape.HEX_VERT: [ - { + { # > 'offset': Vector2(-3.0 / 8.0, 0.0), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [], [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE], ], }, { 'offset': Vector2(-3.0 / 8.0, -0.5), - 'dual_to_display': [ + 'world_to_affected_display_neighbors': [ [], [TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE], [TileSet.CELL_NEIGHBOR_BOTTOM_SIDE], ], - 'display_to_dual': [ + 'display_to_world_neighbors': [ [TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE], [TileSet.CELL_NEIGHBOR_TOP_SIDE], [], diff --git a/addons/TileMapDual/DisplayLayer.gd b/addons/TileMapDual/DisplayLayer.gd index e700c58..486f990 100644 --- a/addons/TileMapDual/DisplayLayer.gd +++ b/addons/TileMapDual/DisplayLayer.gd @@ -1,46 +1,66 @@ +## A single TileMapLayer whose purpose is to display tiles to maintain the Dual Grid illusion. +## Its contents are automatically computed and updated based on: +## - the contents of the parent TileMapDual +## - the rules set in its assigned TerrainLayer class_name DisplayLayer extends TileMapLayer -var dual_to_display: Array -var display_to_dual: Array +## How much to offset this DisplayLayer relative to the main TileMapDual grid. +## This is independent of tile size. var offset: Vector2 + +## When a cell is modified in the parent TileMapDual, +## the DisplayLayer needs to know which of its display cells need to be recomputed. +## This Array stores the paths from the edited cell to the affected display cells. +var world_to_affected_display_neighbors: Array + +## When a display cell needs to be recomputed, +## the TerrainLayer needs to know which tiles surround it. +## This Array stores the paths from the affected cell to the neighboring world cells. +var display_to_world_neighbors: Array + +## See TileSetWatcher.gd var _tileset_watcher: TileSetWatcher + +## See TerrainDual.gd var _terrain: TerrainDual.TerrainLayer + func _init( tileset_watcher: TileSetWatcher, fields: Dictionary, layer: TerrainDual.TerrainLayer ) -> void: #print('initializing Layer...') - tile_set = tileset_watcher.tile_set - dual_to_display = fields.dual_to_display - display_to_dual = fields.display_to_dual offset = fields.offset + world_to_affected_display_neighbors = fields.world_to_affected_display_neighbors + display_to_world_neighbors = fields.display_to_world_neighbors _tileset_watcher = tileset_watcher _terrain = layer + tile_set = tileset_watcher.tile_set tileset_watcher.tileset_resized.connect(reposition, 1) reposition() -func update_tiles_all(cache: Display.CellCache) -> void: +## Updates all display tiles to reflect the current changes. +func update_tiles_all(cache: TileCache) -> void: update_tiles(cache, cache.cells.keys()) -func update_tiles(cache: Display.CellCache, updated_cells: Array) -> void: +func update_tiles(cache: TileCache, updated_cells: Array) -> void: #push_warning('updating tiles') var to_update := Set.new() - for path: Array in dual_to_display: + for path: Array in world_to_affected_display_neighbors: for cell: Vector2i in updated_cells: cell = follow_path(cell, path) if to_update.insert(cell): update_tile(cache, cell) -func update_tile(cache: Display.CellCache, cell: Vector2i) -> void: +func update_tile(cache: TileCache, cell: Vector2i) -> void: var get_cell_at_path := func(path): return get_terrain_at(cache, follow_path(cell, path)) var normalize_terrain := func(terrain): return terrain if terrain != -1 else 0 - var true_neighborhood := display_to_dual.map(get_cell_at_path) + var true_neighborhood := display_to_world_neighbors.map(get_cell_at_path) var is_empty := true_neighborhood.all(func(terrain): return terrain == -1) var terrain_neighborhood = true_neighborhood.map(normalize_terrain) var invalid_neighborhood = terrain_neighborhood not in _terrain.rules @@ -53,7 +73,7 @@ func update_tile(cache: Display.CellCache, cell: Vector2i) -> void: set_cell(cell, sid, tile) -func get_terrain_at(cache: Display.CellCache, cell: Vector2i) -> int: +func get_terrain_at(cache: TileCache, cell: Vector2i) -> int: if cell not in cache.cells: return -1 return cache.cells[cell].terrain diff --git a/addons/TileMapDual/TerrainDual.gd b/addons/TileMapDual/TerrainDual.gd index ef25eb0..dbd6c9f 100644 --- a/addons/TileMapDual/TerrainDual.gd +++ b/addons/TileMapDual/TerrainDual.gd @@ -5,10 +5,12 @@ extends Resource # Functions are ordered top to bottom in the transformation pipeline +## Maps a TileSet to a Neighborhood. static func tileset_neighborhood(tile_set: TileSet) -> Neighborhood: return GRID_NEIGHBORHOODS[Display.tileset_gridshape(tile_set)] +## Maps a GridShape to a Neighborhood. const GRID_NEIGHBORHOODS = { Display.GridShape.SQUARE: Neighborhood.SQUARE, Display.GridShape.ISO: Neighborhood.ISOMETRIC, @@ -19,6 +21,7 @@ const GRID_NEIGHBORHOODS = { } +## A specific neighborhood that the Display tiles will look at. enum Neighborhood { SQUARE, ISOMETRIC, @@ -28,6 +31,7 @@ enum Neighborhood { ## Maps a Neighborhood to a set of atlas terrain neighbors. +# TODO: merge world_to_affected_display_neighbors and display_to_world_neighbors here. const NEIGHBORHOOD_LAYERS := { Neighborhood.SQUARE: [ [ # [] @@ -72,6 +76,7 @@ const NEIGHBORHOOD_LAYERS := { } +## Maps a Neighborhood to a Topology. const NEIGHBORHOOD_TOPOLOGIES := { Neighborhood.SQUARE: Topology.SQUARE, Neighborhood.ISOMETRIC: Topology.SQUARE, @@ -80,22 +85,27 @@ const NEIGHBORHOOD_TOPOLOGIES := { } +## Determines the available Terrain presets for a certain Atlas. enum Topology { SQUARE, TRIANGLE, } +## Swaps the X and Y axes of a Vector2i. static func transposed(v: Vector2i) -> Vector2i: return Vector2i(v.y, v.x) +# TODO: transposed(TileSet.CellNeighbor) -> Tileset.CellNeighbor +## Maps a Neighborhood to a preset of the specified name. static func neighborhood_preset( neighborhood: Neighborhood, - name: String = 'Standard' + preset_name: String = 'Standard' ) -> Dictionary: var topology: Topology = NEIGHBORHOOD_TOPOLOGIES[neighborhood] - var out: Dictionary = PRESETS[topology][name].duplicate(true) + # TODO: check if the preset actually exists + var out: Dictionary = PRESETS[topology][preset_name].duplicate(true) # All Horizontal neighborhoods can be transposed to Vertical if neighborhood == Neighborhood.TRIANGLE_VERTICAL: out.size = transposed(out.size) @@ -105,6 +115,7 @@ static func neighborhood_preset( return out +## Contains all of the builtin Terrain presets for each topology const PRESETS := { Topology.SQUARE: { 'Standard': { @@ -161,6 +172,7 @@ const PRESETS := { } +## Every corner CellNeighbor, in order. const NEIGHBORS: Array[TileSet.CellNeighbor] = [ TileSet.CELL_NEIGHBOR_RIGHT_CORNER, TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER, @@ -172,7 +184,10 @@ const NEIGHBORS: Array[TileSet.CellNeighbor] = [ TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER, ] +# TODO: document the rest +# TODO: extract to file +## A set of rules usable by a single DisplayLayer. class TerrainLayer: extends Resource @@ -237,13 +252,13 @@ func _changed(): emit_changed() +## Create rules for every atlas in a TileSet. func read_tileset(tile_set: TileSet) -> void: terrains = {} layers = [] - neighborhood = Neighborhood.SQUARE + neighborhood = Neighborhood.SQUARE # default if tile_set == null: return - neighborhood = tileset_neighborhood(tile_set) layers = NEIGHBORHOOD_LAYERS[neighborhood].map(TerrainLayer.new) for i in tile_set.get_source_count(): @@ -254,8 +269,8 @@ func read_tileset(tile_set: TileSet) -> void: read_atlas(src, sid) +## Create rules for every tile in an atlas. func read_atlas(atlas: TileSetAtlasSource, sid: int) -> void: - # Read every tile in the atlas var size = atlas.get_atlas_grid_size() for y in size.y: for x in size.x: @@ -266,9 +281,10 @@ func read_atlas(atlas: TileSetAtlasSource, sid: int) -> void: read_tile(atlas, sid, tile) +## Add a new rule for a specific tile in an atlas. func read_tile(atlas: TileSetAtlasSource, sid: int, tile: Vector2i) -> void: var data := atlas.get_tile_data(tile, 0) - var mapping := { 'sid': sid, 'tile': tile } + var mapping := {'sid': sid, 'tile': tile} var terrain_set := data.terrain_set if terrain_set != 0: push_warning( diff --git a/addons/TileMapDual/TileCache.gd b/addons/TileMapDual/TileCache.gd new file mode 100644 index 0000000..6274842 --- /dev/null +++ b/addons/TileMapDual/TileCache.gd @@ -0,0 +1,56 @@ +## Caches the sprite location and terrains of each tile in the TileMapDual world grid. +class_name TileCache +extends Resource + +## Dictionary{ +## key: Vector2i = The coordinates of this tile in the world grid. +## value: Dictionary{ +## 'sid': int = The Source ID of this tile. +## 'tile': Vector2i = The coordinates of this tile in its Atlas. +## 'terrain': int = The terrain assigned to this tile. +## } = The data stored at this tile. +## } +var cells := {} +func _init() -> void: + pass + +## Computes a new TileCache based on the current layer data. +## Needs the old TileCache in case corrections need to made due to accidents. +func compute(tile_set: TileSet, layer: TileMapLayer, cache: TileCache) -> void: + if tile_set == null: + push_error('Attempted to construct TileCache while tile set was null') + return + for cell in layer.get_used_cells(): + # Invalid cells will be treated as empty and ignored + var sid := layer.get_cell_source_id(cell) + if not tile_set.has_source(sid): + continue + var src = tile_set.get_source(sid) + var tile := layer.get_cell_atlas_coords(cell) + if not src.has_tile(tile): + continue + var data := layer.get_cell_tile_data(cell) + if data == null: + continue + # Accidental cells should be reset to their previous value + # They will be treated as unchanged + if data.terrain == -1 or data.terrain_set != 0: + if cell not in cache.cells: + layer.erase_cell(cell) + continue + var cached: Dictionary = cache.cells[cell] + sid = cached.sid + tile = cached.tile + layer.set_cell(cell, cached.sid, cached.tile) + cells[cell] = {'sid': sid, 'tile': tile, 'terrain': data.terrain} + +## Returns the difference between two tile caches +func diff(other: TileCache) -> Array[Vector2i]: + var out: Array[Vector2i] = [] + for key in cells: + if key not in other.cells or cells[key].terrain != other.cells[key].terrain: + out.push_back(key) + for key in other.cells: + if key not in cells: + out.push_back(key) + return out From 2d4355f865ce155ec5c3250cede28e123bb726fd Mon Sep 17 00:00:00 2001 From: raffimolero Date: Thu, 26 Dec 2024 17:31:15 +0800 Subject: [PATCH 4/5] document TerrainLayer --- addons/TileMapDual/TerrainDual.gd | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/addons/TileMapDual/TerrainDual.gd b/addons/TileMapDual/TerrainDual.gd index dbd6c9f..3c86ddc 100644 --- a/addons/TileMapDual/TerrainDual.gd +++ b/addons/TileMapDual/TerrainDual.gd @@ -98,14 +98,19 @@ static func transposed(v: Vector2i) -> Vector2i: # TODO: transposed(TileSet.CellNeighbor) -> Tileset.CellNeighbor +# TODO: Preset.gd + ## Maps a Neighborhood to a preset of the specified name. static func neighborhood_preset( neighborhood: Neighborhood, preset_name: String = 'Standard' ) -> Dictionary: var topology: Topology = NEIGHBORHOOD_TOPOLOGIES[neighborhood] - # TODO: check if the preset actually exists - var out: Dictionary = PRESETS[topology][preset_name].duplicate(true) + # TODO: test when the preset doesn't exist + var available_presets = PRESETS[topology] + if preset_name not in available_presets: + return {'size': Vector2i.ONE, 'sequences': []} + var out: Dictionary = available_presets[preset_name].duplicate(true) # All Horizontal neighborhoods can be transposed to Vertical if neighborhood == Neighborhood.TRIANGLE_VERTICAL: out.size = transposed(out.size) @@ -191,11 +196,26 @@ const NEIGHBORS: Array[TileSet.CellNeighbor] = [ class TerrainLayer: extends Resource + ## A list of which CellNeighbors to care about during terrain checking. var filter: Array = [] + + ## rules: Dictionary{ + ## key: Condition = The terrains that surround this tile. + ## value: { + ## 'sid': int = The Source ID of this tile. + ## 'tile': Vector2i = The coordinates of this tile in its Atlas. + ## } = The sprite that will be chosen when the condition is satisfied. + ## } + ## + ## Condition: Array[ + ## type: int = The terrain found at this position in the filter. + ## size = filter.size() + ## ] var rules: Dictionary = {} func _init(filter: Array) -> void: self.filter = filter + ## Add a new rule for a specific tile in an atlas. func read_tile(data: TileData, mapping: Dictionary) -> void: if data.terrain_set != 0: # This was already handled as an error in the parent TerrainDual @@ -224,18 +244,21 @@ class TerrainLayer: func _condition_to_dict(condition: Array) -> Dictionary: return arrays_to_dict(filter.map(neighbor_name), condition) - ## NOTE: this does not belong here + # NOTE: this does not belong here + ## Merges an Array of keys and an Array of values into a Dictionary. static func arrays_to_dict(keys: Array, values: Array) -> Dictionary: var out := {} for i in keys.size(): out[keys[i]] = values[i] return out - ## NOTE: this does not belong here + # NOTE: this does not belong here + ## Returns a shorthand name for a CellNeighbor. static func neighbor_name(neighbor: TileSet.CellNeighbor) -> String: const DIRECTIONS := ['E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'NE'] return DIRECTIONS[neighbor >> 1] + var neighborhood: Neighborhood var terrains: Dictionary var layers: Array From 931ef02b9347f9970fbbe96607d7e77ef15f3b14 Mon Sep 17 00:00:00 2001 From: raffimolero Date: Sat, 28 Dec 2024 09:18:44 +0800 Subject: [PATCH 5/5] document everything --- addons/TileMapDual/Display.gd | 3 --- addons/TileMapDual/DisplayLayer.gd | 38 +++++++++++++++------------- addons/TileMapDual/Set.gd | 24 +++++++++++++++--- addons/TileMapDual/TerrainDual.gd | 18 +++++++++++-- addons/TileMapDual/TileCache.gd | 13 ++++++++++ addons/TileMapDual/TileMapDual.gd | 1 + addons/TileMapDual/TileSetWatcher.gd | 1 + 7 files changed, 72 insertions(+), 26 deletions(-) diff --git a/addons/TileMapDual/Display.gd b/addons/TileMapDual/Display.gd index 398013a..fc9cda9 100644 --- a/addons/TileMapDual/Display.gd +++ b/addons/TileMapDual/Display.gd @@ -4,9 +4,6 @@ class_name Display extends Node -const TODO = null - - ## See TerrainDual.gd var terrain: TerrainDual ## See TileSetWatcher.gd diff --git a/addons/TileMapDual/DisplayLayer.gd b/addons/TileMapDual/DisplayLayer.gd index 486f990..24f9335 100644 --- a/addons/TileMapDual/DisplayLayer.gd +++ b/addons/TileMapDual/DisplayLayer.gd @@ -41,30 +41,35 @@ func _init( tileset_watcher.tileset_resized.connect(reposition, 1) reposition() +## Adjusts the position of this DisplayLayer based on the tile set's tile_size +func reposition() -> void: + position = offset * Vector2(_tileset_watcher.tile_size) ## Updates all display tiles to reflect the current changes. func update_tiles_all(cache: TileCache) -> void: update_tiles(cache, cache.cells.keys()) -func update_tiles(cache: TileCache, updated_cells: Array) -> void: +## Update all display tiles affected by the world cells +func update_tiles(cache: TileCache, updated_world_cells: Array) -> void: #push_warning('updating tiles') - var to_update := Set.new() + var already_updated := Set.new() + # The order of these two for loops does not matter. for path: Array in world_to_affected_display_neighbors: - for cell: Vector2i in updated_cells: - cell = follow_path(cell, path) - if to_update.insert(cell): - update_tile(cache, cell) + for world_cell: Vector2i in updated_world_cells: + var display_cell := follow_path(world_cell, path) + if already_updated.insert(display_cell): + update_tile(cache, display_cell) +## Updates a specific world cell. func update_tile(cache: TileCache, cell: Vector2i) -> void: - var get_cell_at_path := func(path): return get_terrain_at(cache, follow_path(cell, path)) - var normalize_terrain := func(terrain): return terrain if terrain != -1 else 0 + var get_cell_at_path := func(path): return cache.get_terrain_at(follow_path(cell, path)) var true_neighborhood := display_to_world_neighbors.map(get_cell_at_path) var is_empty := true_neighborhood.all(func(terrain): return terrain == -1) var terrain_neighborhood = true_neighborhood.map(normalize_terrain) - var invalid_neighborhood = terrain_neighborhood not in _terrain.rules - if is_empty or invalid_neighborhood: + var is_invalid_neighborhood = terrain_neighborhood not in _terrain.rules + if is_empty or is_invalid_neighborhood: erase_cell(cell) return var mapping: Dictionary = _terrain.rules[terrain_neighborhood] @@ -73,17 +78,14 @@ func update_tile(cache: TileCache, cell: Vector2i) -> void: set_cell(cell, sid, tile) -func get_terrain_at(cache: TileCache, cell: Vector2i) -> int: - if cell not in cache.cells: - return -1 - return cache.cells[cell].terrain +# TODO: move some of these to TerrainDual +## Coerces all empty tiles to have a terrain of 0. +static func normalize_terrain(terrain): + return terrain if terrain != -1 else 0 +## Finds the neighbor of a given cell by following a path of CellNeighbors func follow_path(cell: Vector2i, path: Array) -> Vector2i: for neighbor: TileSet.CellNeighbor in path: cell = get_neighbor_cell(cell, neighbor) return cell - - -func reposition() -> void: - position = offset * Vector2(_tileset_watcher.tile_size) diff --git a/addons/TileMapDual/Set.gd b/addons/TileMapDual/Set.gd index c59a7e6..8e94b6f 100644 --- a/addons/TileMapDual/Set.gd +++ b/addons/TileMapDual/Set.gd @@ -1,50 +1,66 @@ -## Real sets don't exist yet +## Real sets don't exist yet. ## https://github.com/godotengine/godot/pull/94399 class_name Set extends Resource + +## The internal Dictionary that holds this Set's items as keys. var data: Dictionary = {} func _init(initial_data: Variant = []) -> void: union_in_place(initial_data) +## Returns true if the item exists in this Set. func has(item: Variant) -> bool: return item in data + +## A dummy value to put in a slot. +const DUMMY = null ## Returns true if the item was not previously in the Set. func insert(item: Variant) -> bool: var out := not has(item) - data[item] = null + data[item] = DUMMY return out + ## Returns true if the item was previously in the Set. func remove(item: Variant) -> bool: return data.erase(item) + +## Deletes all items in this Set. func clear() -> void: data = {} + ## Merges an Array's items or Dict's keys into the Set. func union_in_place(other: Variant): for item in other: insert(item) + +## Returns a new Set with the items of both self and other. func union(other: Set) -> Set: var out = self.duplicate() out.union_in_place(other.data) return out + ## Removes an Array's items or Dict's keys from the Set. func diff_in_place(other: Variant): for item in other: remove(item) + +## Returns a new Set with all items in self that are not present in other. func diff(other: Set) -> Set: var out = self.duplicate() out.diff_in_place(other.data) return out -## Given an Array or Dict, + +## Inserts elements that are in other but not in self, and removes elements found in both. func xor_in_place(other: Variant): for item in other: if has(item): @@ -52,6 +68,8 @@ func xor_in_place(other: Variant): else: insert(item) + +## Returns a new Set where each item is either in self or other, but not both. func xor(other: Set) -> Set: var out = self.duplicate() out.xor_in_place(other.data) diff --git a/addons/TileMapDual/TerrainDual.gd b/addons/TileMapDual/TerrainDual.gd index 3c86ddc..18be609 100644 --- a/addons/TileMapDual/TerrainDual.gd +++ b/addons/TileMapDual/TerrainDual.gd @@ -3,6 +3,7 @@ class_name TerrainDual extends Resource + # Functions are ordered top to bottom in the transformation pipeline ## Maps a TileSet to a Neighborhood. @@ -189,7 +190,6 @@ const NEIGHBORS: Array[TileSet.CellNeighbor] = [ TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER, ] -# TODO: document the rest # TODO: extract to file ## A set of rules usable by a single DisplayLayer. @@ -259,8 +259,13 @@ class TerrainLayer: return DIRECTIONS[neighbor >> 1] +## The Neighborhood type of this TerrainDual. var neighborhood: Neighborhood + +## Maps a terrain type to its sprite as registered in the TerrainDual. var terrains: Dictionary + +## The TerrainLayers for this TerrainDual. var layers: Array var _tileset_watcher: TileSetWatcher func _init(tileset_watcher: TileSetWatcher) -> void: @@ -269,6 +274,8 @@ func _init(tileset_watcher: TileSetWatcher) -> void: _changed() +## Emitted when any of the terrains change. +## NOTE: Prefer connecting to TerrainDual.changed instead of TileSetWatcher.terrains_changed. func _changed(): #print('SIGNAL EMITTED: changed(%s)' % {}) read_tileset(_tileset_watcher.tile_set) @@ -339,13 +346,14 @@ static func write_default_preset(tile_set: TileSet, atlas: TileSetAtlasSource) - ) write_preset( atlas, - NEIGHBORHOOD_LAYERS[neighborhood], + NEIGHBORHOOD_LAYERS[neighborhood], # TODO: can we just pass in the neighborhood right away neighborhood_preset(neighborhood), terrain_offset + 0, terrain_offset + 1, ) +## Adds 2 new terrain types to terrain set 0 for the sprites to use. static func create_false_terrain_set(tile_set: TileSet, terrain_name: String) -> int: if tile_set.get_terrain_sets_count() == 0: tile_set.add_terrain_set() @@ -358,6 +366,10 @@ static func create_false_terrain_set(tile_set: TileSet, terrain_name: String) -> return terrain_offset +## Takes a preset and puts it onto the given atlas. +## ARGUMENTS: +## - atlas: the atlas source to apply the preset to. +## - filters: the neighborhood filter static func write_preset( atlas: TileSetAtlasSource, filters: Array, @@ -391,6 +403,8 @@ static func write_preset( atlas.get_tile_data(tile_fg, 0).terrain = terrain_foreground +## Unregisters all the tiles in an atlas and changes the size of the +## individual sprites to accomodate a size.x by size.y grid of sprites. static func clear_and_resize_atlas(atlas: TileSetAtlasSource, size: Vector2i): # Clear all tiles atlas.texture_region_size = atlas.texture.get_size() + Vector2.ONE diff --git a/addons/TileMapDual/TileCache.gd b/addons/TileMapDual/TileCache.gd index 6274842..d138b19 100644 --- a/addons/TileMapDual/TileCache.gd +++ b/addons/TileMapDual/TileCache.gd @@ -2,6 +2,9 @@ class_name TileCache extends Resource + +## Maps a cell coordinate to the stored tile data +## ## Dictionary{ ## key: Vector2i = The coordinates of this tile in the world grid. ## value: Dictionary{ @@ -14,6 +17,7 @@ var cells := {} func _init() -> void: pass + ## Computes a new TileCache based on the current layer data. ## Needs the old TileCache in case corrections need to made due to accidents. func compute(tile_set: TileSet, layer: TileMapLayer, cache: TileCache) -> void: @@ -44,6 +48,7 @@ func compute(tile_set: TileSet, layer: TileMapLayer, cache: TileCache) -> void: layer.set_cell(cell, cached.sid, cached.tile) cells[cell] = {'sid': sid, 'tile': tile, 'terrain': data.terrain} + ## Returns the difference between two tile caches func diff(other: TileCache) -> Array[Vector2i]: var out: Array[Vector2i] = [] @@ -54,3 +59,11 @@ func diff(other: TileCache) -> Array[Vector2i]: if key not in cells: out.push_back(key) return out + + +## Returns the terrain value of the tile at the given cell coordinates. +## Empty cells have a terrain of -1. +func get_terrain_at(cell: Vector2i) -> int: + if cell not in cells: + return -1 + return cells[cell].terrain diff --git a/addons/TileMapDual/TileMapDual.gd b/addons/TileMapDual/TileMapDual.gd index 62b33a5..e55cd19 100644 --- a/addons/TileMapDual/TileMapDual.gd +++ b/addons/TileMapDual/TileMapDual.gd @@ -34,6 +34,7 @@ func _make_self_invisible() -> void: material.light_mode = CanvasItemMaterial.LightMode.LIGHT_MODE_LIGHT_ONLY +## HACK: How long to wait before processing another "frame" @export var map_refresh_cooldown: float = 0.0 var _timer: float = 0.0 func _process(delta: float) -> void: # Only used inside the editor diff --git a/addons/TileMapDual/TileSetWatcher.gd b/addons/TileMapDual/TileSetWatcher.gd index ca0233f..1223437 100644 --- a/addons/TileMapDual/TileSetWatcher.gd +++ b/addons/TileMapDual/TileSetWatcher.gd @@ -59,6 +59,7 @@ func _atlas_added(source_id: int, atlas: TileSetAtlasSource): var _flag_terrains_changed := false ## Emitted when an atlas is added or removed, ## or when the terrains change in one of the Atlases. +## NOTE: Prefer connecting to TerrainDual.changed instead of TileSetWatcher.terrains_changed. signal terrains_changed func _terrains_changed(): #print('SIGNAL EMITTED: terrains_changed(%s)' % {})