Change individual tile properties

Godot Version

4.2 stable

Question

Hi! I fight with this issue for some time and cannot grasp how to do it properly.
First of all I am still very new to Godot and I tried reading through docs, looking for guides regarding this or similiar topic, but I am stuck.

What I want to achieve is that on collision with given tile, I want to edit and change this tile’s Light Mask.
I somewhat managed to achieve it by instantiating scene with rigid body on each tile, then on collision RigidBody2d property was changed. But this way of doing it seems very tedious and is hard to work with later.

Currently i managed to get cords of the tile that CharacterBody2d is colliding (thanks to some guides from previous versions of Godot).
But I can’t access light layer property from it.

func _on_main_collided(collider):
	#collider - CharacterBody2d.get_last_slide_collision()
	if collider.get_collider() is TileMap:
		var tile_collided = collider.get_collider().local_to_map(Global.joe_pos)
		var tile_relative_pos = Vector2i(collider.get_normal())  
		var tile = tile_collided - tile_relative_pos

again what I want is to get tile (Vector2i) cords and use it to change light mask of this one tile only.

thank you!

Oh yea, I know this problem.

You have to save all tiles in an array / dictionary. Get the node from the dictionary and then make any changes that you want to do to it. (I have found no workarounds to this)

Note: you need to refresh the tile map before you save the tile.

1 Like

Thanks!
ok didn’t even think about it.
one question, what do you mean to refresh the tile map before saving?

One way of doing this is:

update_internals()
saved_tiles[hovering_tile]=get_child(get_children().size()-1);

hovering_tile is the tile coordinates.

Note that this code runs on the tilemap node. So if you want to run the code for a specific tilemap would be something like this

get_node(“tilemap”).update_internals();
get_node(“tilemap”).saved_tiles[hovering_tile]=get_node(“tilemap”).get_child(get_node(“tilemap”).get_children().size()-1);

In my case I am building the tilemap as the game is running, the tiles are actually children of the tilemap node. Since you can not know what child has been added I just take the last child, but the get_children().size() does not return the correct size in due time, so you have to call update_internals().

I am not sure if update_internals() is necessary in your specific case, but I recommend adding it just to make sure.

1 Like

Thank you again,
Way beyond of what I could come up with I must admit.

I think that closes the topic :slight_smile:

Sorry to came again but I think I am stuck even earlier tan updating internals.
by adding tiles in an array I get array of Vector2i of all tiles in main scene.
So I am basically in the same place as before, can’t access properties only with array of cords and cord of my tile.

If I try to add TileMap.get_children() to an array, array is empty.

how can I add tiles so I can access them later?
sorry to bother but I just can’t wrap my head around it

I’m afraid I do not have the time to build an example for you atm. I may be able to make one in the next few days, but, I make no promises.

The basics are already in place. My guess is that you need just to make some more experiments until you get something to work.

Here is a short code example from my project:

func _input(event):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
curent_mouse_event = event
else:
curent_mouse_event = event

func _process(delta):
var players :Array = players_data.players
var curent_player_index :int= players_data.current_player_index
var hovering_tile:Vector2i = local_to_map(get_local_mouse_position());
if( curent_mouse_event.pressed):
set_cell(2, hovering_tile, 1 ,Vector2i(0, 0), players[curent_player_index][‘tile’])
update_internals();
saved_tiles[hovering_tile]=get_child(get_children().size()-1);
update_self_and_neighbors_score(hovering_tile);

func update_self_and_neighbors_score(coords :Vector2i ):
var value:int = saved_tiles[coords].axis_values


Here is the summary of what happens in that code:

I am using the mouse to add a new tile and then save it.
You may want to do a separate demo scene with a similar example to test things.
The tile is a scene, not just a regular tile.
then I save the node into a Dictionary

When I need to update a custom property (from a script attached on that node), I use the coords of that tile to obtain the node from the dictionary.

Just change players[curent_player_index][‘tile’] with the tile id that you are using.

If you show me a code example of how you coded this, I may be able to give some feedback.

I know my answer is a bit complex, but this will have to do for now.

Let me know if this helps.

Hey thank you very much, I will sit on it more tomorrow and maybe it will get me somewhere :slight_smile: If I get something i will post it here

Here is a better example:

extends TileMap

var stupid_tiles      :Dictionary             = {}
var curent_mouse_event:InputEventMouseButton  = null

func _ready():
	print(get_children().size()) #prints 0
	update_internals()
	print(get_children().size()) #prints 21
	for child in get_children():
		stupid_tiles[local_to_map(child.position)]=child;

func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_LEFT:
			if event.pressed:
				curent_mouse_event = event
			else:
				curent_mouse_event = event
#
## Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	var hovering_tile:Vector2i = local_to_map(get_local_mouse_position());
	if null != curent_mouse_event and curent_mouse_event.pressed:
		print(stupid_tiles[hovering_tile].value_for_each_four_axis)
		stupid_tiles[hovering_tile].value_for_each_four_axis[0] += 1
		print(stupid_tiles[hovering_tile].value_for_each_four_axis)

image


image

here is the script attached to the tile:

extends Area2D
var value_for_each_four_axis = [1,1,1,1]

Ok. now i see what you did.
I am afraid I already had this solution before and I just wasn’t happy with it, it seems like it can’t be THE solution.

so from what I see you use tile map but instead of tile set you instantiate scenes through it.

that was what I managed before

I make TileSet with SceneCollection of 4 walls I want to use to make a room
Issue no.1 you can’t see much which scene you want to put where because of

then I create a level with those scenes
issue no.2 you can’t see where you put what because it’s just a thin line but it’s almost the same as in normal tile set, yet placing those is more uncomfortable

Set up of each scene is area + rigid body with code that reacts to collision - code is shared with all scenes, it’s just prototype so it can be smaller (just StaticBody is sufficient)
Issue no.3 I have 8 scenes just for square room, example of one of the walls
code:

extends StaticBody2D

@onready var Sprite = $"../../Sprite2D"

var immune: bool = false

func collision():
	print(get_parent())
	if immune == false:
		if Sprite.light_mask < 2:
			Sprite.light_mask = 2
			immune = true
			await get_tree().create_timer(0.5).timeout
			immune = false
	else:
		pass

final in game product:
Desktop2024.05.26-21.23.55.01-ezgif.com-video-to-gif-converter

So ye, that seems to work but if I just manage to change light mask on the tile I collide with I can skip all those steps and use TileMap.


My current version no.2 used one of the lines from your suggestion and works just with TileMap but requires two TileMaps because I can just erase a collided cell, not change it’s properties.
In this case I make two TileMaps of the same room, each on different LightMask

setting up is doubled but it is easier than with scenes as previously so it’s kind of an upgrade I think…

On main scene I have two separate light sources, one makes all dark - LightLayer1 second gives lights - LightLayer2:

Then on collision I emit signal to TileMap with information of last collider:

func collision_control():
	
	var collider = joe.get_last_slide_collision()

	if collider == null:
		pass
	else:
		var col = collider.get_collider()
		collided.emit(collider)

code in tile:

func _on_main_collided(collider):
	
	if collider.get_collider() is TileMap:
		var tile_collided = collider.get_collider().local_to_map(Global.joe_pos)
		var tile_relative_pos = Vector2i(collider.get_normal())  
		tile = tile_collided - tile_relative_pos							###tile###
		level_design.set_cell(0, tile, 0, tile, 0)

final product is smoother but still not perfect because I need to make double tile map which might be clunky and cause mistakes if I want to make maps bigger:
GodotEngineNvidiaProfile2024.05.26-21.49.09.02-ezgif.com-video-to-gif-converter

sooo

Finally in my last line of code (“code in tile” pic), instead of just deleting collided cell I could change it’s light layer I can make it all clear and working well.

I see, it looks very nice, congratulations!

I am not sure that I fully understand the first solution but here is a short recap of my understanding and some of things I struggle to understand.

There are 8 tiles for walls (left right top bottom and corners).

  1. Issue no.1&2: I am sure you have already considered this but in your place what I would do is add a tick line that I can hide at the end.
    When working on my project I tried to avoid adding nodes fearing that this will slow down the game. In my project I had to add a label that displayed the values of some properties that were changed by code. I kept adding then removing then adding it back again. In the end I just left it there… it makes debugging much easier and if I the game needs to be optimized I can easily remove it.
  2. Issue no.3 I mean you wont need 8 scenes if you just make a square wall, then you will have only 1 scene to work with =) (it is probably possible to make just one side of the wall light up depending on collision). You can also have 2 walls and on collision just add one on top of the other. This way you do not need to add the walls into an array.

For version no.2 I do not think you need to duplicate the tilemap.

If my understanding is right, what you did is create a room on tilemap1 and then create the same room on tilemap2 (duplicate it)
When a collision happens you delete the tile from one of the tilemaps.
This is not really necessary,

What I would do is have a tilemap with the room and an empty tilemap on top.
When the collision happens add the same tile on tilemap no 2 (you will need to make sure the tiles have the same id) after a little bit you can add a timer to remove the tile from layer 1 (may not be really necessary to remove it)

As a 3rd solution you may want to look into the modulate property. Set up 2 layers on one tilemap, modulate one to black. when a collision happens add a tile on the white layer (I am not sure if you can just move it from one layer to another).

Good luck with the project! Hope this gives you a new perspective.

I played with it a lot and I think I will just go with solution with square wall and just smaller tiles, not best but works.

Shame there is no option to just change property of one tile that you place it’d help me alot, but I believe not many people use it then :slight_smile:

thanks a lot for your help I think it just is what it is :smiley: