161 lines
5.9 KiB
GDScript
161 lines
5.9 KiB
GDScript
@tool
|
|
@icon('TileMapDual.svg')
|
|
class_name TileMapDual
|
|
extends TileMapLayer
|
|
|
|
|
|
## An invisible material used to hide the world grid so only the display layers show up.
|
|
## Currently implemented as a shader that sets all pixels to 0 alpha.
|
|
var _ghost_material: Material = preload("res://addons/TileMapDual/ghost_material.tres")
|
|
|
|
|
|
# === External functions that don't exist once exported ===
|
|
# HACK: this uses some sort of "Dynamic Linking" technique because these features don't exist right now
|
|
# - conditional compilation
|
|
# - static signals
|
|
static func _editor_only(name: String):
|
|
push_error('Attempt to call Editor-Only function "' + name + '"')
|
|
static var autotile: Callable = _editor_only.bind('autotile').unbind(3)
|
|
static var popup: Callable = _editor_only.bind('popup').unbind(2)
|
|
|
|
|
|
## Material for the display tilemap.
|
|
@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "ShaderMaterial, CanvasItemMaterial")
|
|
var display_material: Material:
|
|
get:
|
|
return display_material
|
|
set(new_material): # Custom setter so that it gets copied
|
|
display_material = new_material
|
|
changed.emit()
|
|
|
|
var _tileset_watcher: TileSetWatcher
|
|
var _display: Display
|
|
func _ready() -> void:
|
|
_tileset_watcher = TileSetWatcher.new(tile_set)
|
|
_display = Display.new(self, _tileset_watcher)
|
|
add_child(_display)
|
|
_make_self_invisible(true)
|
|
if Engine.is_editor_hint():
|
|
_tileset_watcher.atlas_autotiled.connect(_atlas_autotiled)
|
|
set_process(true)
|
|
else: # Run in-game using signals for better performance
|
|
changed.connect(_changed, 1)
|
|
set_process(false)
|
|
# Update full tileset on first instance
|
|
await get_tree().process_frame
|
|
_changed()
|
|
|
|
|
|
## Automatically generate terrains when the atlas is initialized.
|
|
func _atlas_autotiled(source_id: int, atlas: TileSetAtlasSource):
|
|
autotile.call(source_id, atlas, tile_set)
|
|
|
|
|
|
## Keeps track of use_parent_material to see when it turns on or off.
|
|
var _cached_use_parent_material = null
|
|
##[br] Makes the main world grid invisible.
|
|
##[br] The main tiles don't need to be seen. Only the DisplayLayers should be visible.
|
|
##[br] Called every frame, and functions a lot like TileSetWatcher.
|
|
func _make_self_invisible(startup: bool = false) -> void:
|
|
# If user has set a material in the original slot, inform the user
|
|
if material != _ghost_material:
|
|
if not startup and Engine.is_editor_hint():
|
|
popup.call(
|
|
"Warning! Direct material edit detected.",
|
|
"Don't manually edit the real material in the editor! Instead edit the custom 'Display Material' property.\n" +
|
|
"(Resetting the material to an invisible shader material... this is to keep the 'World Layer' invisible)\n" +
|
|
"* This warning is only given when the material is set in the Godot editor.\n" +
|
|
"* In-game scripts may set the material directly. It will be copied over to display_material automatically."
|
|
)
|
|
else:
|
|
# copy over the material if it was edited by script
|
|
display_material = material
|
|
material = _ghost_material # Force TileMapDual's material to become invisible
|
|
|
|
# check if use_parent_material is set
|
|
if (
|
|
Engine.is_editor_hint()
|
|
and use_parent_material != _cached_use_parent_material
|
|
and _cached_use_parent_material == false # cache may be null
|
|
):
|
|
popup.call(
|
|
"Warning: Using Parent Material.",
|
|
"The parent material will override any other materials used by the TileMapDual,\n" +
|
|
"including the 'ghost shader' that the world tiles use to hide themselves.\n" +
|
|
"This will cause the world tiles to show themselves in-game.\n" +
|
|
"\n" +
|
|
"* Recommendation: Turn this setting off. Don't use parent material.\n" +
|
|
"* Workaround: Set your world tiles to custom sprites that are entirely transparent.\n" +
|
|
"(see 'res://addons/TileMapDual/docs/custom_drawing_sprites.mp4' for a non-transparent example)"
|
|
)
|
|
_cached_use_parent_material = use_parent_material
|
|
|
|
|
|
## HACK: How long to wait before processing another "frame".
|
|
## Mainly matters when [godot_4_3_compatibility] is active.
|
|
@export_range(0.0, 0.1) var refresh_time: float = 0.02
|
|
var _timer: float = 0.0
|
|
func _process(delta: float) -> void: # Only used inside the editor
|
|
if refresh_time < 0.0:
|
|
return
|
|
if _timer > 0:
|
|
_timer -= delta
|
|
return
|
|
_timer = refresh_time
|
|
call_deferred('_changed')
|
|
|
|
## When toggled on, double-checks ALL cells in the grid every change.
|
|
## Only use this when running Godot 4.3 and below,
|
|
## where TileMapLayer could not detect changes properly.
|
|
@export var godot_4_3_compatibility: bool = _godot_is_below_4_4()
|
|
|
|
## Detects if godot is below v4.4.
|
|
## Only used to detect whether the _update_cells() function is usable.
|
|
func _godot_is_below_4_4():
|
|
var version := Engine.get_version_info()
|
|
return version.major < 4 or version.major == 4 and version.minor < 4
|
|
|
|
## Called by signals when the tileset changes,
|
|
## or by _process inside the editor.
|
|
func _changed() -> void:
|
|
_tileset_watcher.update(tile_set)
|
|
|
|
var updated_cells := []
|
|
# HACK: double check all tiles every refresh
|
|
if godot_4_3_compatibility and tile_set != null:
|
|
var current_cells := TileCache.new()
|
|
current_cells.update(self, get_used_cells())
|
|
updated_cells = current_cells.xor(_display.cached_cells)
|
|
|
|
_display.update(updated_cells)
|
|
_make_self_invisible()
|
|
|
|
|
|
## Called when the user draws on the map or presses undo/redo.
|
|
func _update_cells(coords: Array[Vector2i], forced_cleanup: bool) -> void:
|
|
if is_instance_valid(_display):
|
|
_display.update(coords)
|
|
|
|
|
|
##[br] Public method to add and remove tiles.
|
|
##[br]
|
|
##[br] - 'cell' is a vector with the cell position.
|
|
##[br] - 'terrain' is which terrain type to draw.
|
|
##[br] - terrain -1 completely removes the tile,
|
|
##[br] - and by default, terrain 0 is the empty tile.
|
|
func draw_cell(cell: Vector2i, terrain: int = 1) -> void:
|
|
var terrains := _display.terrain.terrains
|
|
if terrain not in terrains:
|
|
erase_cell(cell)
|
|
changed.emit()
|
|
return
|
|
var tile_to_use: Dictionary = terrains[terrain]
|
|
var sid: int = tile_to_use.sid
|
|
var tile: Vector2i = tile_to_use.tile
|
|
set_cell(cell, sid, tile)
|
|
changed.emit()
|
|
|
|
## Public method to get the terrain at a specific coordinate.
|
|
func get_cell(cell: Vector2i) -> int:
|
|
return get_cell_tile_data(cell).terrain
|