Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TileMapDual v5 #16

Open
raffimolero opened this issue Dec 12, 2024 · 25 comments · Fixed by #29
Open

TileMapDual v5 #16

raffimolero opened this issue Dec 12, 2024 · 25 comments · Fixed by #29
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@raffimolero
Copy link
Collaborator

raffimolero commented Dec 12, 2024

Supporting hex grids is a huge task. It requires having 2 separate TileSet resources for the parent and child. But I've got ideas.

I manually cropped and assembled this tileset from https://youtu.be/Uxeo9c-PX-w?t=428 (the same frame used in the readme) which I hope could help develop triangular and hex grids further. It's a bit complicated since it's a "tall" tileset, so I'll be looking for a simpler one for now.
Hex Dual - 64x83tri - 85x72hex

Each triangle sprite is 64x83, and when pieced together they become 85x72 hex tiles. I don't know how the math works yet.
Unfortunately, Godot does not support triangular grids. Fortunately, they're just square grids with extra steps.
I think the geometry of the display_tilemap can be computed as follows:

display_tilemap.tile_set.tile_shape = TileSet.TileShape.TILE_SHAPE_SQUARE
display_tilemap.tile_set.tile_size = Vector2(self.tile_set.tile_size) * Vector2(1, 0.5)
display_tilemap.position = i_dont_know_yet()

But you'd need display_tilemap to have a different tile_set for this to work. It can't reuse the parent's.

@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 12, 2024

reference

Note: base = min diameter, not side

hexagons

@raffimolero
Copy link
Collaborator Author

intended goal
image
image

@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 12, 2024

I've made a flat hex dual tileset with a better layout which will hopefully be usable for this project. Math is hard, but it's fun.

Hex Dual Vert Flat - 48x64tri 64x64hex
Simpler tileset Preview

@pablogila pablogila added enhancement New feature or request help wanted Extra attention is needed labels Dec 12, 2024
@pablogila
Copy link
Owner

I added the "help wanted" label because I am afraid I do not have enough experience with hex grids, and sadly I am too busy these weeks to figure it out...
But this would be such a great addition to the code! Please don't hesitate to share these implementations, I will follow them closely :D

@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 12, 2024

Ok so the main problem here is that the Dual Layer is Hexagonal, but the Display Layer is Triangular/Square with different dimensions. This essentially means we have to store 2 separate TileSets, and... sync them...

Things like changing the Z-Axis of specific tiles might be important to someone later on, among other things in whatever data layers there are. I'll have to figure out how to deep copy a TileSet before anything else can be done.

Edit: praise be Resource.changed() and Resource.duplicate() thank godot

@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 13, 2024

@pablogila I basically had to rewrite the whole grid system for this... Hex Grid Github branch here.

The grid now automatically creates Terrains for any given TileSet if it recognizes the layout. This works for Square, Isometric, and most importantly Hexagonal templates, and supports multiple atlases too.

I'm currently rewriting the whole Dual Grid system itself, as well. By the end of this patch, 80 to 90% of the code will look entirely different...

auto.hex.terrain.mp4

Most of the code is now very modular. I have a Grids.gd file that stores all the important information about each kind of tiling. Templates, offsets, and neighbors for Square, Isometric, Half Offset Squares and Hexagonal grids with Vertical and Horizontal axes... but not all of the data are filled in yet.

The different layouts (Stacked, Stacked Offset, Stairs Right/Down, etc...) are different coordinate systems where the x,y positions have quirky logic for each one, which means I can't easily fake the dual grid with a smaller rectangular grid. So now I will resort to storing 2 separate display grids for Hex (and later Half Offset Square)

As for the update logic, I don't have anything yet, but that will come next. Basically it will:

  • compare how it last remembers the world grid (cached tilemap data) versus how it looks now (get used cells by id)
  • take the set xor of them
  • find all the dual neighbors of those cells (using GRID_DATA dual_to_display)
  • update all the duals with their world neighbors (USING GRID_DATA display_to_dual and template)
  • cache how it looks now

@pablogila
Copy link
Owner

This is super cool! I ported the code into a regular addon to make it easier to work with a modular structure, I really love seeing contributions go this way!

At this point, it might be interesting to create a state machine to handle each grid layout. This would definitely make them easier to implement and, most importantly, much easier to maintain. One of my biggest fears now is touching a line of code for square tiles that breaks the hex implementation and so on. A state machine might be a solution for this. What should be the logic behind it is definitely a thing to check. I might be a bit lost in the hex part of the implementation, but I can definitely work on the square and iso stuff!

I'm upgrading my laptop this weekend, so I don't have my usual tools yet, but I'll get into it as soon as I can. You are introducing some really cool ideas, that is so nice!

@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 14, 2024

Alright so I haven't done much implementation work but I've come up with some more ideas for the ideal future of this plugin.

Since there are so many different grids, I've decided that instead of a constants file, I'm going to just store the display layers as separate scenes. Edit: idea scrapped. constants are easier to work with.
The dual_to_display and display_to_dual bits probably shouldn't be exported, actually. I could just keep them in the script.
image

When a new tile_set is loaded:

  • It figures out the TileSet's Topology based on the tile_shape and tile_layout
  • a specific topologies/*.tres will be instantiated
  • its internal TileMapLayers' positions will be multiplied based on the tile_set.tile_size
  • [see later] the AtlasLayout (Edit: Now TerrainDual) will be computed based on the terrains and peering bits of every single sprite atlas source
  • the cell neighborhood, and cell positions to update will all just be passed in whenever the displays need to be updated

To get the AtlasLayout, we will automatically inspect each tile in the TileSet's many atlases and looking at their terrains and peering bits. They will be configured just like any normal Godot terrain would, with "Match Corners" as the mode.
Configuring these is a bit of a hassle, so the plugin should just auto-generate them like before.
image
image

Drawing tiles is a bit scuffed. Since I'm using the actual Godot terrain drawing system, using the real terrains sometimes overrides tiles that you've already drawn before. It'd be fine if I could guarantee which tiles might pop up, but I don't know.
image

@raffimolero raffimolero changed the title Hexagonal tiles are not supported Full Rewrite Dec 14, 2024
@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 14, 2024

This is very ambitious... I'll just call it what it is.

I initially just wanted to implement hex grids, but now this project will probably have to stay as a fork. I'm not sure if making a pull request would make sense, since it's entirely different.

Repo branch here:
https://github.com/raffimolero/TileMapDual/tree/hex-double-layer

@pablogila
Copy link
Owner

It makes sense as long as the end user does not notice the difference. For now, it is definitely wiser to work in a fork. I will check the code and try to contribute what I can. Let's see what comes of it!

@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 22, 2024

@pablogila This took 10 whole days...

The reason this is so complicated is because of all the custom terrain auto-generation stuff. It's actually probably slower than your code because it stores a whole mapping for every valid combination of tiles. I think it's way too complicated for a simple 2-tile terrain, but it's able to support an unlimited number of different terrains. Dirt with grass, sand, and water in the same atlas, for example. But you'll have to set up the terrain connections manually, for every terrain combination you need.

Unfortunately I'll be busy soon, and there's still a lot of unreliable parts with this system. I don't completely understand how the signal system works. It can't really replace the current TileMapDual, since it works so differently, but... It works. That's a huge win in my book.

Details on how to set it up are in a video in the latest README of the current branch: https://github.com/raffimolero/TileMapDual/tree/dev

2024-12-22_19-09-15.mp4
2024-12-22_19-31-31.mp4

Right now all Grid Shapes and Offset Axes are supported:

  • Square
  • Isometric
  • Half-Offset Square (Horizontal and Vertical)
  • Hexagon (Horizontal and Vertical)

Reactivity is currently janky. See the issues in my repo for more info.

@raffimolero
Copy link
Collaborator Author

raffimolero commented Dec 24, 2024

I don't think I understood just how flexible the terrain configuration system was:

label.configuration.mp4
terrain.swapping.mp4

This engine is quite powerful; it's able to produce live feedback and control multiple TileMapLayers at once using a single World layer. It's also internally capable of recognizing terrain patterns from further away. Given time and effort (which I won't spend) it's possible to configure it so that it can control all of a map's visuals in real time, based on only some world layers. All they'd need to do is configure Display.GRIDS to have more layers and rulesets.

@pablogila
Copy link
Owner

Wow, this work is amazing! First of all, thanks for sharing!

Godot is about to implement an on_cell_changed signal, which might be handy (see #7). Personally, I was waiting for it before touching any more code, it might be worth a try!

I have no clue what is going on in your last video, I will definitely have to check it out as it seems so useful.
I don't want to bore you with my life, but I've had a problem with a new laptop and it's probably going to be on maintenance until January. So unfortunately I won't be able to play with the code as much as I expected to over the holidays :/ Still, I am following your fork closely!

@raffimolero
Copy link
Collaborator Author

Practically all of the functionality I want has been implemented in the fork!

terrain.autogen.mp4

From terrain auto-generation, to supporting multiple different atlases in the same TileSet, this version pretty much has all the basic features you'd want... except documentation, and editor performance until on_cell_changed is implemented... but both of our versions seem to have the same problem.

It's not perfect, but I'm happy with how most of it turned out. It's just as easy to set up as your current version, too. Once the documentation is updated, it could even be good enough to merge, if you want.

@pablogila
Copy link
Owner

This is such a great piece of code! Thanks a lot for sharing your huge contribution with everyone.
I will take an in-depth look at it, I want to really understand it before messing it up.
Definitely the on_cell_changed is going to be a huge deal here.

Just want to ask, how compatible is this fork with the use of shaders? Cause I have been trying some implementations here, and it was such a pain... Some of these modifications could be imported to the new version, but I want to go slow and steady not to break anything... And if I can help you with the documentation, I am willing to do so as well :D

I will do some checks these days before doing a merge into a new branch if that's ok.

Again, thanks Raffimolero for sharing!

@raffimolero
Copy link
Collaborator Author

raffimolero commented Jan 4, 2025

Thanks for asking - shaders should be no problem. All relevant TileMapLayer properties can just be copied from the parent TileMapDual into every DisplayLayer. that should mostly work, but note that hex and half-offset square tilesets have 2 DisplayLayers.

The codebase is getting pretty large. I've documented most of the technical details, so only the README demos, showcases, and examples are left. Should be easy enough for a user to do, maybe you could test the system to see if it's intuitive.

The Terrain System itself will have to be explained in a later post. I'll give a rundown of how the classes interact at the moment:

Project Structure

TileMapDual (extends TileMapLayer) is the parent of a single Display (extends Node2D), which is the parent of multiple DisplayLayers (extends TileMapLayer).

Only the DisplayLayers should be visible, as they correspond to your current TileMapDual.display_tilemap: TileMapLayer property.

The TileMapDual assigns a TileSetWatcher (stored in a variable) to watch for changes to its tile_set, such as:

  • creating a new tileset
  • deleting the current tileset
  • changing the tile_size, tile_shape, tile_offset_axis, or terrains
  • adding new tile atlases (it assigns one AtlasWatcher for each one, to detect Terrain Auto-Generation)
    If any of those change, the watcher sends an appropriate signal. The TileSetWatcher and AtlasWatcher classes do not do anything on their own, they just watch and send signals.

When any tile_set change happens (other than changing tile_size), the TerrainDual deletes all its TerrainLayers and recreates them. For a Square/Isometric tileset, there is only one TerrainLayer. For a Hex Vertical tileset, there are 2 TerrainLayers, one for "left" facing triangles, one for "right" facing triangles. Each layer recomputes all of its terrains by reading the atlases and looking at the tiles assigned to them. Then the TerrainDual emits a changed signal.

The signal is detected by Display, which deletes all DisplayLayers and recreates them - one for each TerrainLayer - and offsets their positions according to the tile_shape (square/iso/half-off square/hex) and tile_offset_axis (horizontal/vertical). The layers then compute which tiles go where based on the parent TileMapDual's world tiles.

Here's how the "left" DisplayLayer looks like. I removed the terrains for the right-facing triangles.

image

Put 2 of them together, and you get a full grid.

image

Any shaders or other properties will only apply to the DisplayLayers if they are either copied or inherited from the TileMapDual. Note that the relationship is TileMapDual -> Display -> DisplayLayer. Maybe I can eliminate the Display node and keep only the logic, if it's necessary.

@pablogila
Copy link
Owner

I am liking it so far, this is really well designed! If you want, push to a v5.0.0-dev branch, I will give you edit access. Thanks for this summary, it was really useful to understand it!

@raffimolero
Copy link
Collaborator Author

raffimolero commented Jan 7, 2025

I've created and pushed the changes into the dev branch, but I'm afraid I won't be around to maintain this when class starts.

I tried doing a pull request, but the merge conflicts were too much, so I cloned it locally, attempted a merge, and just... manually dumped my current files into the branch to overwrite all the conflicting files. Yes, this means losing progress on the current issues, but we can take a look at the history between my fork and the merge, and try to go from there.

To quote my (now closed) pull request

I'm pushing my branch a bit prematurely here; it's not exactly presentable yet, but much of the remaining work is documentation-related. I'd like to push here so @ pablogila can push changes to it if need be, then I'll just push bugfixes when I find them.

I've recorded all of the relevant videos in docs/*.mp4, so adding them to the README should be as easy as drag-and-drop.

I'll continue work on this here, instead of in my fork. Fixing merge conflicts is one thing, but fixing bugs and catching up with every other issue is another. This might take a while.

In the meantime, do play around with it! You can start by reconstructing all the examples. This branch needs a lot of testing, and I can't catch most bugs since I mainly use the program only as intended.

@pablogila
Copy link
Owner

Great! It's ok, we can try to port some additions later down the line.

Let's treat this as a v5-dev for now. If anybody wants to test the new version and include modifications or report bugs, of course it is more than welcome :D

@raffimolero
Copy link
Collaborator Author

I don't plan on implementing this unless necessary, but I've figured out how to make the child nodes show up.

Inspired by godotengine/godot-proposals#10572, here's a little proof of concept, where I showed the internal Display node:

2025-01-08_16-51-56.mp4

@pablogila
Copy link
Owner

Really interesting. Indeed, some people was asking how to see these layers.
Of course it should not be necessary, but now that we might add again an exported material for shaders, we could include an exported section called dev or similar, with a show_in_editor bool to enable this view. Or at least during the dev period. Please let me know your thoughts, do you think it could be useful without introducing confusion to new users?

@raffimolero
Copy link
Collaborator Author

raffimolero commented Jan 10, 2025

do you think it could be useful without introducing confusion to new users?

I'm not sure. It's definitely useful, but very likely fragile and would need careful work.

It will probably need a note that says "These display layers are read-only" and a "Refresh" button somewhere in case the user modifies something on accident. Being able to see the nodes means being able to edit them, but only the main TileMapDual node's properties are followed. It might surprise users if they try to edit properties on Layer 0 and 1, but then they get overwritten later.

I'm going to be busy with other projects very soon, too. I might come back now and then, but progress will slow down. It's been a fun month, and there's been a lot of progress, but the work isn't done.

Feel free to ask me about anything and everything. I'm active on Discord, and I have a Slack account, so I could send you my username via email if you'd like to chat.

@raffimolero
Copy link
Collaborator Author

raffimolero commented Jan 26, 2025

I fixed the undo/redo issue with terrain autogen, after a long while... things should be more stable than usual now.

We still have to fix the other bugs with shaders and such. I've already managed to pass in the parent TileMapDual into DisplayLayer._init(), so all that's left is to:

  • copy the parent's fields into each DisplayLayer
  • set the opacity instead of setting the material

and that should fix them the same way it did in v4.

Maybe v5.1 or .2 could have the separate, visible world layer. Maybe it'll be much later. Maybe it should be in Godot itself, heh. But contributing directly to the engine is hard when solutions are still supposed to be under discussion.

@raffimolero raffimolero changed the title Full Rewrite TileMapDual v5 Jan 26, 2025
@pablogila
Copy link
Owner

Okay, I introduced some changes. I added the TileMapDualLegacy node, a copy of the stable version, so that the transition to v5 is smoother. I also implemented some changes in the in-editor refresh_time, to avoid refreshing the full tilemap at _process().

I am a bit worried about single cell updates. Creating a medium-sized map had my pc fans at full speed, since the full map is being updated at _process(). On the contrary, the legacy version only updates the very single cell that has changed. This is quite interesting for performance reasons, especially for big maps, but I am not yet sure how to achieve this in the v5 version...

@pablogila
Copy link
Owner

Since the legacy version is already bundled with the v5 version, I think it is time to merge branches. We need more eyes on this and making v5 the default branch seems a reasonable tradeoff... We can keep this issue opened for further discussion.

@pablogila pablogila linked a pull request Feb 2, 2025 that will close this issue
@pablogila pablogila reopened this Feb 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants