Skip to content

Commit 199c109

Browse files
Implement isometric & hexagonal tilemaps (#1213)
* Start working on isometric tiles * Get isometric cell position * Fix isometric mask * Draw the appropriate grid depending on the cell shape * Fix pixel to isometric cell positions and vice versa * Formatting * Placing a tile on another tile replaces it instead of mixing * Undo works on new cells * Add a scale_factor variable in tile mode indices * Better horizontal/vertical cells calculation, still not a proper solution yet * Slightly improve cell amount * Add get_tile_size() and get_tile_shape() methods in CelTileMap * Fix shadowed variables * Implement basic locking logic * When the tilemap is locked, ensure that all cel portions are updated Needed for overlapping isometric tiles * Remove previous tile to avoid overlapped pixels * Add a tile_layout variable and some docstrings * Formatting * Create isometric tilesets from the UI * Make a tilemap layer into place only mode from the layer settings And pass the variables down from the layer to the cel * Fix crash when cloning tilemap layers * Re-order tilemap when changing its settings * Restrict users to place tiles mode only when tilemap layer is in place only mode * Fix manual mode (I think) * Fix layer properties nodes not updating when selecting another layer * Move tool brings tiles from outside of the canvas and makes them visible * Fix rectangular tiles on place only mode, when the tilemap cell is larger than the tileset cell * All of the available canvas should have initialized tilemap cells now * Format * Fix resizing on non-place only mode * Change offset when resizing canvas * Change offset when cropping to content, selection and centering frames * Resize image resizes the tileset cells * Fix scaling to non-integer multiples creating duplicate-ish tiles, and don't emit tileset.updated when resizing, as this affects all of the tilemaps in that project * Fix resizing locked isometric tilemaps Currently this breaks crop to content/selection and makes the image blank, the offset needs fixing anyway * Fix crop to content/selection offsets * Fix loading place only tilemaps * Don't allow merging layers into a place-only tilemap layer * Set cel's tile size and shape when setting tileset * Undo/redo for locking tilemap and changing tile, shape & layout * Disable selection resizing on isometric tilemaps It is not working for isometric tilemaps at the moment. If we find a solution, we should re-enable it. * Do not apply effects to place-only tilemaps * Scaling project updates brush preview * Warn users that locking a tilemap layer is permanent * Copying and linking tilemap cels copies the indices Needed for place-only tilemaps with overlapping pixels * Add tile_added, tile_removed and tile_replaced signals in the tileset script * Improve generate_isometric_rectangle algorithm Not perfect but should be better * Add strings for translations * Scale indices based on cell size * Fix crash when switching between tilemaps with different tilesets, and the new selected tiles do not exist in the new tileset * Small improvement in tile indicator preview * Support hexagonal tiles in place-only mode Not currently supported in normal mode, as we need a mask for that first. * Fully support pointy-top hexagonal tiles * Fix grid type enum name * Add tile_offset_axis variables * Support flat top hexagons in the backend Needs UI now * Expose tile_offset_axis in the UI and add a duplicate method to TileSetCustom * Change tileset shape & offset axis when importing image * Fix unused variables * Update Translations.pot * Fix crash when using the bucket tool * Minor code improvements * Use the stack mode when drawing with the draw tiles mode enabled Avoids issues such as replacing tiles, or changing their order
1 parent 4bbd4bb commit 199c109

35 files changed

+1222
-232
lines changed

Diff for: Translations/Translations.pot

+65
Original file line numberDiff line numberDiff line change
@@ -3623,6 +3623,7 @@ msgstr ""
36233623

36243624
#. A tileset is a collection of tiles.
36253625
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
3626+
#: src/UI/Timeline/LayerProperties.tscn
36263627
msgid "Tileset:"
36273628
msgstr ""
36283629

@@ -3640,9 +3641,73 @@ msgid "Tileset name:"
36403641
msgstr ""
36413642

36423643
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
3644+
#: src/UI/Timeline/LayerProperties.tscn
36433645
msgid "Tile size:"
36443646
msgstr ""
36453647

3648+
#. The shape of each tile in a tileset (rectangular, isometric, hexagonal).
3649+
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
3650+
#: src/UI/Timeline/LayerProperties.tscn
3651+
msgid "Tile shape:"
3652+
msgstr ""
3653+
3654+
#. The layout that the tiles appear in, in a non-rectangular grid.
3655+
#: src/UI/Timeline/LayerProperties.tscn
3656+
msgid "Tile layout:"
3657+
msgstr ""
3658+
3659+
#. The offset axis (horizontal or vertical) of non-rectangular tiles.
3660+
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
3661+
#: src/UI/Timeline/LayerProperties.tscn
3662+
msgid "Tile offset axis:"
3663+
msgstr ""
3664+
3665+
#. Tooltip of the "tile offset axis" option.
3666+
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
3667+
#: src/UI/Timeline/LayerProperties.tscn
3668+
msgid "For all half-offset shapes (Isometric & Hexagonal), determines the offset axis."
3669+
msgstr ""
3670+
3671+
#. A tile layout option.
3672+
#: src/UI/Timeline/LayerProperties.tscn
3673+
msgid "Stacked"
3674+
msgstr ""
3675+
3676+
#. A tile layout option.
3677+
#: src/UI/Timeline/LayerProperties.tscn
3678+
msgid "Stacked offset"
3679+
msgstr ""
3680+
3681+
#. A tile layout option.
3682+
#: src/UI/Timeline/LayerProperties.tscn
3683+
msgid "Stairs right"
3684+
msgstr ""
3685+
3686+
#. A tile layout option.
3687+
#: src/UI/Timeline/LayerProperties.tscn
3688+
msgid "Stairs down"
3689+
msgstr ""
3690+
3691+
#. A tile layout option.
3692+
#: src/UI/Timeline/LayerProperties.tscn
3693+
msgid "Diamond right"
3694+
msgstr ""
3695+
3696+
#. A tile layout option.
3697+
#: src/UI/Timeline/LayerProperties.tscn
3698+
msgid "Diamond down"
3699+
msgstr ""
3700+
3701+
#. A button that toggles place-only mode on for a tilemap layer. This mode is used to only place tiles in a tilemap, without the ability to modify them.
3702+
#: src/UI/Timeline/LayerProperties.tscn
3703+
msgid "Place-only mode:"
3704+
msgstr ""
3705+
3706+
#. A confirmation dialog that appears when attempting to enable place-only mode in a tilemap layer.
3707+
#: src/UI/Timeline/LayerProperties.tscn
3708+
msgid "Enabling place-only mode is a permanent action. Once activated, you will only be able to place tiles, and you won't be able to modify existing tiles anymore on this layer."
3709+
msgstr ""
3710+
36463711
#: src/UI/TilesPanel.gd
36473712
msgid "Select a tile to place it on the canvas."
36483713
msgstr ""

Diff for: src/Autoload/DrawingAlgos.gd

+103-14
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,86 @@ func similar_colors(c1: Color, c2: Color, tol := 0.392157) -> bool:
560560
)
561561

562562

563+
func generate_isometric_rectangle(image: Image) -> void:
564+
var half_size := image.get_size() / 2
565+
var up := Vector2i(half_size.x, 0)
566+
var right := Vector2i(image.get_size().x, half_size.y)
567+
if image.get_height() < image.get_width():
568+
up += Vector2i.LEFT
569+
elif image.get_height() > image.get_width():
570+
up += Vector2i.DOWN
571+
right += Vector2i.DOWN
572+
var up_right := Geometry2D.bresenham_line(up, right)
573+
up_right.pop_back()
574+
up_right.pop_back()
575+
for pixel in up_right:
576+
image.set_pixelv(pixel, Color.WHITE)
577+
var left := Vector2i(image.get_size().x - 1 - pixel.x, pixel.y)
578+
for j in range(pixel.x, left.x - 1, -1):
579+
image.set_pixel(j, pixel.y, Color.WHITE)
580+
var mirror_y := Vector2i(j, image.get_size().y - 1 - pixel.y)
581+
for k in range(pixel.y, mirror_y.y + 1):
582+
image.set_pixel(j, k, Color.WHITE)
583+
var mirror_right := Vector2i(pixel.x, image.get_size().y - 1 - pixel.y)
584+
image.set_pixelv(mirror_right, Color.WHITE)
585+
586+
587+
func generate_hexagonal_pointy_top(image: Image) -> void:
588+
var half_size := image.get_size() / 2
589+
var quarter_size := image.get_size() / 4
590+
var three_quarters_size := (image.get_size() * 3) / 4
591+
var up := Vector2i(half_size.x, 0)
592+
var quarter := Vector2i(image.get_size().x - 1, quarter_size.y)
593+
var line := Geometry2D.bresenham_line(up, quarter)
594+
for pixel in line:
595+
image.set_pixelv(pixel, Color.WHITE)
596+
var mirror := Vector2i(image.get_size().x - 1 - pixel.x, pixel.y)
597+
for j in range(pixel.x, mirror.x - 1, -1):
598+
image.set_pixel(j, pixel.y, Color.WHITE)
599+
var three_quarters := Vector2i(image.get_size().x - 1, three_quarters_size.y - 1)
600+
line = Geometry2D.bresenham_line(quarter, three_quarters)
601+
for pixel in line:
602+
image.set_pixelv(pixel, Color.WHITE)
603+
var mirror := Vector2i(image.get_size().x - 1 - pixel.x, pixel.y)
604+
for j in range(pixel.x, mirror.x - 1, -1):
605+
image.set_pixel(j, pixel.y, Color.WHITE)
606+
var down := Vector2i(half_size.x, image.get_size().y - 1)
607+
line = Geometry2D.bresenham_line(three_quarters, down)
608+
for pixel in line:
609+
image.set_pixelv(pixel, Color.WHITE)
610+
var mirror := Vector2i(image.get_size().x - 1 - pixel.x, pixel.y)
611+
for j in range(pixel.x, mirror.x - 1, -1):
612+
image.set_pixel(j, pixel.y, Color.WHITE)
613+
614+
615+
func generate_hexagonal_flat_top(image: Image) -> void:
616+
var half_size := image.get_size() / 2
617+
var quarter_size := image.get_size() / 4
618+
var three_quarters_size := (image.get_size() * 3) / 4
619+
var left := Vector2i(0, half_size.y)
620+
var quarter := Vector2i(quarter_size.x, image.get_size().y - 1)
621+
var line := Geometry2D.bresenham_line(left, quarter)
622+
for pixel in line:
623+
image.set_pixelv(pixel, Color.WHITE)
624+
var mirror := Vector2i(pixel.x, image.get_size().y - 1 - pixel.y)
625+
for j in range(pixel.y, mirror.y - 1, -1):
626+
image.set_pixel(pixel.x, j, Color.WHITE)
627+
var three_quarters := Vector2i(three_quarters_size.x - 1, image.get_size().y - 1)
628+
line = Geometry2D.bresenham_line(quarter, three_quarters)
629+
for pixel in line:
630+
image.set_pixelv(pixel, Color.WHITE)
631+
var mirror := Vector2i(pixel.x, image.get_size().y - 1 - pixel.y)
632+
for j in range(pixel.y, mirror.y - 1, -1):
633+
image.set_pixel(pixel.x, j, Color.WHITE)
634+
var down := Vector2i(image.get_size().x - 1, half_size.y)
635+
line = Geometry2D.bresenham_line(three_quarters, down)
636+
for pixel in line:
637+
image.set_pixelv(pixel, Color.WHITE)
638+
var mirror := Vector2i(pixel.x, image.get_size().y - 1 - pixel.y)
639+
for j in range(pixel.y, mirror.y - 1, -1):
640+
image.set_pixel(pixel.x, j, Color.WHITE)
641+
642+
563643
# Image effects
564644
func center(indices: Array) -> void:
565645
var project := Global.current_project
@@ -587,11 +667,15 @@ func center(indices: Array) -> void:
587667
continue
588668
var cel_image := (cel as PixelCel).get_image()
589669
var tmp_centered := project.new_empty_image()
590-
tmp_centered.blend_rect(cel.image, used_rect, offset)
670+
tmp_centered.blend_rect(cel_image, used_rect, offset)
591671
var centered := ImageExtended.new()
592672
centered.copy_from_custom(tmp_centered, cel_image.is_indexed)
593673
if cel is CelTileMap:
594-
(cel as CelTileMap).serialize_undo_data_source_image(centered, redo_data, undo_data)
674+
var tilemap_cel := cel as CelTileMap
675+
var tilemap_offset := (offset - used_rect.position) % tilemap_cel.get_tile_size()
676+
tilemap_cel.serialize_undo_data_source_image(
677+
centered, redo_data, undo_data, tilemap_offset
678+
)
595679
centered.add_data_to_dictionary(redo_data, cel_image)
596680
cel_image.add_data_to_dictionary(undo_data)
597681
project.deserialize_cel_undo_data(redo_data, undo_data)
@@ -603,20 +687,26 @@ func center(indices: Array) -> void:
603687
func scale_project(width: int, height: int, interpolation: int) -> void:
604688
var redo_data := {}
605689
var undo_data := {}
690+
var tilesets: Array[TileSetCustom] = []
606691
for cel in Global.current_project.get_all_pixel_cels():
607692
if not cel is PixelCel:
608693
continue
609694
var cel_image := (cel as PixelCel).get_image()
610-
var sprite := _resize_image(cel_image, width, height, interpolation) as ImageExtended
695+
var sprite := resize_image(cel_image, width, height, interpolation) as ImageExtended
611696
if cel is CelTileMap:
612-
(cel as CelTileMap).serialize_undo_data_source_image(sprite, redo_data, undo_data)
697+
var tilemap_cel := cel as CelTileMap
698+
var skip_tileset_undo := not tilesets.has(tilemap_cel.tileset)
699+
tilemap_cel.serialize_undo_data_source_image(
700+
sprite, redo_data, undo_data, Vector2i.ZERO, skip_tileset_undo, interpolation
701+
)
702+
tilesets.append(tilemap_cel.tileset)
613703
sprite.add_data_to_dictionary(redo_data, cel_image)
614704
cel_image.add_data_to_dictionary(undo_data)
615705

616706
general_do_and_undo_scale(width, height, redo_data, undo_data)
617707

618708

619-
func _resize_image(
709+
func resize_image(
620710
image: Image, width: int, height: int, interpolation: Image.Interpolation
621711
) -> Image:
622712
var new_image: Image
@@ -664,7 +754,9 @@ func crop_to_selection() -> void:
664754
var cropped := ImageExtended.new()
665755
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed)
666756
if cel is CelTileMap:
667-
(cel as CelTileMap).serialize_undo_data_source_image(cropped, redo_data, undo_data)
757+
var tilemap_cel := cel as CelTileMap
758+
var offset := rect.position
759+
tilemap_cel.serialize_undo_data_source_image(cropped, redo_data, undo_data, -offset)
668760
cropped.add_data_to_dictionary(redo_data, cel_image)
669761
cel_image.add_data_to_dictionary(undo_data)
670762

@@ -703,7 +795,9 @@ func crop_to_content() -> void:
703795
var cropped := ImageExtended.new()
704796
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed)
705797
if cel is CelTileMap:
706-
(cel as CelTileMap).serialize_undo_data_source_image(cropped, redo_data, undo_data)
798+
var tilemap_cel := cel as CelTileMap
799+
var offset := used_rect.position
800+
tilemap_cel.serialize_undo_data_source_image(cropped, redo_data, undo_data, -offset)
707801
cropped.add_data_to_dictionary(redo_data, cel_image)
708802
cel_image.add_data_to_dictionary(undo_data)
709803

@@ -724,13 +818,8 @@ func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> voi
724818
resized.convert_rgb_to_indexed()
725819
if cel is CelTileMap:
726820
var tilemap_cel := cel as CelTileMap
727-
var skip_tileset := (
728-
offset_x % tilemap_cel.tileset.tile_size.x == 0
729-
and offset_y % tilemap_cel.tileset.tile_size.y == 0
730-
)
731-
tilemap_cel.serialize_undo_data_source_image(
732-
resized, redo_data, undo_data, skip_tileset
733-
)
821+
var offset := Vector2i(offset_x, offset_y)
822+
tilemap_cel.serialize_undo_data_source_image(resized, redo_data, undo_data, offset)
734823
resized.add_data_to_dictionary(redo_data, cel_image)
735824
cel_image.add_data_to_dictionary(undo_data)
736825

Diff for: src/Autoload/OpenSave.gd

+13-3
Original file line numberDiff line numberDiff line change
@@ -882,15 +882,22 @@ func import_reference_image_from_image(image: Image) -> void:
882882

883883

884884
func open_image_as_tileset(
885-
path: String, image: Image, horiz: int, vert: int, project := Global.current_project
885+
path: String,
886+
image: Image,
887+
horiz: int,
888+
vert: int,
889+
tile_shape: TileSet.TileShape,
890+
tile_offset_axis: TileSet.TileOffsetAxis,
891+
project := Global.current_project
886892
) -> void:
887893
image.convert(project.get_image_format())
888894
horiz = mini(horiz, image.get_size().x)
889895
vert = mini(vert, image.get_size().y)
890896
var frame_width := image.get_size().x / horiz
891897
var frame_height := image.get_size().y / vert
892898
var tile_size := Vector2i(frame_width, frame_height)
893-
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file())
899+
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file(), tile_shape)
900+
tileset.tile_offset_axis = tile_offset_axis
894901
for yy in range(vert):
895902
for xx in range(horiz):
896903
var cropped_image := image.get_region(
@@ -906,13 +913,16 @@ func open_image_as_tileset_smart(
906913
image: Image,
907914
sliced_rects: Array[Rect2i],
908915
tile_size: Vector2i,
916+
tile_shape: TileSet.TileShape,
917+
tile_offset_axis: TileSet.TileOffsetAxis,
909918
project := Global.current_project
910919
) -> void:
911920
image.convert(project.get_image_format())
912921
if sliced_rects.size() == 0: # Image is empty sprite (manually set data to be consistent)
913922
tile_size = image.get_size()
914923
sliced_rects.append(Rect2i(Vector2i.ZERO, tile_size))
915-
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file())
924+
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file(), tile_shape)
925+
tileset.tile_offset_axis = tile_offset_axis
916926
for rect in sliced_rects:
917927
var offset: Vector2 = (0.5 * (tile_size - rect.size)).floor()
918928
var cropped_image := Image.create(

Diff for: src/Classes/AsepriteParser.gd

+6-3
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,14 @@ static func open_aseprite_file(path: String) -> void:
210210
var _diagonal_flip_bitmask := ase_file.get_32()
211211
ase_file.get_buffer(10) # Reserved
212212
var tilemap_cel := cel as CelTileMap
213-
var tileset := tilemap_cel.tileset
214213
@warning_ignore("integer_division")
215214
var bytes_per_tile := bits_per_tile / 8
216215
var tile_data_compressed := ase_file.get_buffer(
217216
chunk_size - TILEMAP_CEL_CHUNK_SIZE
218217
)
218+
var tile_size := tilemap_cel.get_tile_size()
219219
var tile_data_size := (
220-
width * height * tileset.tile_size.x * tileset.tile_size.y * pixel_byte
220+
width * height * tile_size.x * tile_size.y * pixel_byte
221221
)
222222
var tile_data := tile_data_compressed.decompress(
223223
tile_data_size, FileAccess.COMPRESSION_DEFLATE
@@ -391,7 +391,10 @@ static func open_aseprite_file(path: String) -> void:
391391
data_length, FileAccess.COMPRESSION_DEFLATE
392392
)
393393
var tileset := TileSetCustom.new(
394-
Vector2i(tile_width, tile_height), tileset_name, false
394+
Vector2i(tile_width, tile_height),
395+
tileset_name,
396+
TileSet.TILE_SHAPE_SQUARE,
397+
false
395398
)
396399
for k in n_of_tiles:
397400
var n_of_pixels := tile_width * tile_height * pixel_byte

0 commit comments

Comments
 (0)