Help with TileMap interaction from left side and bottom of each tile

Godot Version

Godot 4.4

Question

Hello, I am working on making an object to behave as a drill that can dig through tiles in a TileMap. I’m relatively new and very novice in understanding. Currently, I’m using the unique name of my tilemap in the level as a variable in the gd script attached to the drill object.

I am able to get this working great when coming in from the top side and right side, using a Raycast2d (set up below), but when coming in from the left side or bottom side, it doesn’t take the cell away like I was hoping. What’s weird is I can get it to show the coordinates and print like the collision is working fine, it just wont remove the cell.

I have tried using set_cell() and end up with the same issue, and functionality as when I use erase_cell. I’ve also tried to set this up in a gd script attached to the tile map so it would maybe be a more direct way to erase the cell, but I am stuck with the same problem. So I went back to having it all take place in the drill script so I have nothing scripted to the tile map and is half working as before. Do you know if I am missing an important step here for removing the cells or am I stuck with top and right side functionality? Here is the code I have for the drill:

func _physics_process(delta: float) -> void:

	_drilled_here()

func _drilled_here():
	if ray_cast_2d.is_colliding():
		var collision_point = ray_cast_2d.get_collision_point()
		var collider = ray_cast_2d.get_collider()
		if collider:
			if collider.is_in_group("MudWall"):
				var tile_pos = mid.to_local(collision_point)
				var tile_spot = mid.local_to_map(tile_pos)
				mid.erase_cell(tile_spot)

mid is what I named the layer

Thank you for reading.

Not sure if this helps but I found that the source id of the tile is coming in as -1 when colliding from the left side or bottom and what I found says that means it thinks the tile is empty but I’m not sure why.

So, my guess it has to do with the collision point returning a coord. So, when you convert the coord to map it is returning a map coord that doesnt exist.

I don’t know if this is the case, you could test it by making a single cell, and printing the three coords from each direction when drilled, global, local and map.

If it is this, one solution maybe to make the collision shape on the tiles not go to the edge on the left and bottom sides. So that the raycast detects further into the cell.

I would say test the coords like I suggested to see if that is the issue.

1 Like

Thank you, I think you are on it. I’m trying to set up a way to test which side I am colliding from and I’m completely stumped again. I found another post on this forum from a few yrs ago that says to get the collision object’s normal and normalize the raycast’s vector, then rotate it, and multiply them together and check whether it’s > 1 and it’s laid out there in plain english but I can’t understand it or apply it.

I was able to get it to show the ID of the tile I was colliding with and going at it from the left and bottom sides brings back -1.

From the top and right it’s returning the correct ID from the point of collision and then goes to -1.

I also tried using area2d and had the exact same thing happens. Which is what makes me think you’re right, and it’s saying the collision coordinates are hitting a nonexistent tile on the map. What I don’t understand is why this is happening from the left and bottom and not right and top or how to correct it.

My thought for a little while now has been if I can figure if it’s colliding from left and bottom, or maybe just saying if the tile id is -1 then add a number to the coordinates that it has received from the collision, but I think that is way off too. Thanks for giving me a place to start on this. It’s fun but man I’m having a hard time wrapping my head around it

A solution would be to eliminate the need of raycast and collision entirely.

If you know where your drill is, and which direction it is facing and its range rhat it can drill. You can write a func that simply checks if there is a tile at x coord.

X = drill_coord + (face_dir * range), you convert this to map to check if there is a tile there.

This might be a bit more code to implement possibly, but it avoids using raycast or collision detection.

Tonight I’ll see about implementing a minimalist test (good excuse to procrastinate on improving my behavior tree logic in my project).

Theoreticcaly it could also be in your tilemap setup as well, I don’t remember if there is an offset for layers that’d make the topright the origin of tile.

1 Like

I keep running into walls even with removing the need for collisions now I’m considering how to get the facing direction and…I’m not getting it, I found some info about getting the rotation, so would I take the

drill.global_position or maybe

drill.position.x + (drill.rotation * drill_length)

drill_length being a variable I set by putting 2 markers at each end of the drill and subtracting those 2 positions and calling .length on that result?

Now I haven’t tested this but it already feels like I’m missing the point in what you’re saying and taking the wrong pieces.

Sorry for my misunderstanding. I really appreciate you working with me on this. I will happily be a procrastination partner but I’m sure one with the basic level of understanding I have can be less than stellar to hang around with

No worries man, figuring out facing direction can be daunting first time you are handling it.

In godot I believe the default “facing” direction is -x without any weird rotation.

So, you should be able to do something like this if your drill rotates.

global_position + (Vector2.UP * drill_distance).rotate(rotation)

var drill_distance: = 100

This should check within 100 pixels of your drill position, in the direction it is pointing, if your drill sprite rotates and is facing default direction. If it isn’t facing default direction you can experiment to find its facing direction.

You can do a simple test in physics_process.

If input is pressed:
global_position += (Vector2.UP * 100).rotate(rotation)

Use UP, DOWN, LEFT, RIGHT until you find which matches your sprites facing direction.

This is a method I used for an older turnbased game I was messing around with.

How you choose to figure out drill length depends on your game. If the drill doesn’t change sizes you can just measure its tip from the center of the drill and set that as drill length. I would say that would be easier, you can always change that value in code later if you upgrade drill length or have powerups that make the drill larger or something.

1 Like

Thank you very much, if I ever wrap my head around this I’m sure I’ll remember it forever lol I finally thought I wrapped my head around signals, and while I can implement them, I still don’t really understand them or where exactly to use them. But on this issue, I think I was able to get the coordinates of the drill local to tile map how you said. By doing:
var x = global_position + (Vector2.UP * drill_distance)
var tile_pos = mid.to_local(x)
var tile_spot = mid.local_to_map(tile_pos)

I still cannot wrap my head around the rotation part of it, but I figure if it can just be in the spot and check if the tile is there and delete it, that would still be perfect.

But now I cannot figure how to check if the tile is actually there to delete it. Would I set an empty tile variable? then fill it in with the coordinates I get from the tile spot? but I’m confused as to how it would check for tile presence.

extends Node2D #World, or common parent of drill and tilemap

#This just connects the signal from drill to tilemap
func _ready():
	$Sprite2D.check_tile.connect($TileMap.find_tile)
extends TileMap #Tilemap

#This finds if there is a cell at location passed by player
func find_tile(tile_pos) -> void:
#This converts global_position passed by player to map coords
	var map_tile_pos = local_to_map(to_local(tile_pos))
#This removes the cell at the coords
	set_cell(0,map_tile_pos,-1)
extends Sprite2D #Drill

#This is signal to check if there is a tile to be removed
signal check_tile(pos)

#This is how far away from drill to check/ and this is the coord to send
var drill_distance: = 16
var drill_pos: Vector2

func _input(event):
#This moves the drill and checks 16 pixels(size of cell) away from player
	if Input.is_action_just_pressed("north"):
		global_position = global_position + (Vector2.UP * drill_distance).rotated(rotation)
		drill_pos = global_position + (Vector2.UP * drill_distance).rotated(rotation)
#This sends out signal to check if there is a destroyable tile
		emit_signal("check_tile", drill_pos)
#These were just to rotate "drill" for purpose of testing
	if Input.is_action_just_pressed("east"):
		global_rotation_degrees += 90
	if Input.is_action_just_pressed("west"):
		global_rotation_degrees -= 90

This is my mock up, and it works to destroy tile, obviously it is a minimalist recreation, but I hope it gives ideas to help you move forward.

I added signal functionality to give you a real world application of signal usage since you mentioned struggling with signals.

The logic behind the rotation check, is that it takes to “default” direction -x, and uses it but rotated to agree with the rotation of the node. So, in this case it will always give 16 pixels in the direction the node is facing, because it makes the calculation relative to the rotation value of the node. Hopefully that makes a little sense.

Rotated() rotates the Vector2 by some value, when you place rotation (which is the value of rotation of the node) it will rotate the Vector to agree with the players rotation, so technically from a global_position standpoint it is no longer -x, but some other Vector.

2 Likes

Wow thank you so much. I am going to try replicating what you have here and seeing if I can get the functionality working and then go from there. Again thank you! I am super excited to play around with this in game. Won’t be able to until a little later my time, but you’ve given me something to look forward to trying when I get there :slight_smile:

I couldn’t wait. I implemented it how you said to do it and it just works. It works exactly how you said it would. While I can sort of connect the dots, I get that a signal is being sent between the drill and tile map, and you explained everything to me I just still don’t fully get it. Going to sit on this one for a while and really try following the dots to see if I can ever grasp what you just did for me. Thank you a million times over.

So, for communicating between scripts in Godot there are two basic methods.

Calling a function directly and sending out a signal.

The principle in coding is that you don’t want things having hard dependencies if at all possible. Hard dependencies is for example one node directly calling another node, in Godot you can do this with $SomeNodeAddressHere.

Example:

func some_player_func():
     $SomeOtherNodeFunc.some_func()

However, the principle is that you can use direct calling, but you want to generally call down the tree. So, to have a parent call a func in a child node. However, you don’t want that child then calling a func in its parent node. So, the child can send a signal up the node tree.

This follows the principle call down, signal up.

This is a principle and there is flexibility, it is there to keep you from breaking your code by deleting a dependency (also, practically if you are using $SomeNode a lot, and you change that nodes location or name, now you have broken references).

Now, signals, if you need to send out information, however, the node doesn’t know who needs that information, or where that other node is, you can use signals.

In my example, my drill doesn’t know where the tilemap is. It has no reference for my tilemap, however the mutual parent always knows its children, so the parent connects the two childrens signals. The beauty of signals is that nothing returns an error if there is no one who hears the signal.

An example, say you have pineapples that spawn, and your player eats the pineapples, well they disappear after you eat them, also the player doesn’t know where they are. So, the player sends out a signal which is connected by mutual parent to eventually arrive at the pineapple, and destroy the pineapple. However, even if the player does the action to eat a pineapple when there is no pineapple, nothing bad happens. This would be different if the player was trying to call the non-existant pineapple directly.

Also, since it took me awhile to discover the joys of return, I will try and explain them to you here.

If you have a parent node, and it wants some info from its child that isn’t stored in a variable, but rather calculated in a func. The parent can call the func, and the func can send a signal back to the parent.

However, the other wonder is the return keyword. With this keyword you can return a value back to the original caller.

func some_func() -> void: #Means it returns void, aka nothing
	#I want some info from child
	var needed_info = $SomeChild.some_other_func()
	what_i_need_info_for = needed_info
	#I now used that needed info that was stored in the variable
	
func some_other_func() -> Int: #Means it return an integer
	var needed_info_cal
	#Does some calculations
	return needed_info_cal

Wow I just man I appreciate you taking the time and saying it to me. I feel like I can wrap my head around how that works with signals, but I still just scratch my head. I think I can see the point in your example how the pineapple and character are both in the parent level, but I would be lying to myself if I said “I get it!” lol I believe it will take a lot of time (if any amount at all) to where I can put this information to practical use. I will always be chasing these concepts around until hopefully one day I get a hold of them. Was there any tutorial you watched that really helped you see why all of these pieces fit together how they do? Your explanations have been great I just almost need like an ABCs 123s for exactly where my head is at in all of this stuff which is just an impossible thing to search for. Appreciate all your help. You have gotten me motivated!

You can check this out, some light reading talking about signal principles.

Signal GDScript explanation

1 Like

I appreciate the link. I’ll check this out. Hope everything goes smoothly with your project tonight! You whooped my deal so fast it probably didn’t even take your mind off it for more than a minute lol

So, not sure if you figured out your problem, or just deleted it because you didn’t want to bother me.

So, I’ll try to explain

global_position = global_position + (Vector2.UP * drill_distance).rotated(rotation)
	drill_pos = global_position + (Vector2.UP * drill_distance).rotated(rotation)

The global_position part is for moving. So, a nodes default direction is Vector2.UP, or -x.

The first part sets the new direction to be current position (global_position) + the default facing direction * how far you want to move (drill_distance). However that would only ever move up, if you left it like this.

So, the second part of that is to make the (Vector2.UP * drill_distance) a value relative to the node facing direction, rather then a set direction. To do this we use the rotated() function that rotates the value of a Vector2. We need to tell it what value to rotate it to though.

To get the value relative to the nodes current facing direction we use the Node2D property rotation, that gives us how the current node is rotated, and rotates our movement Vector2 to be the same as our nodes rotation.

Example:

If I rotate the node to be facing right, the nodes rotation property changes, so, when I move now, my Vector2.UP * drill_distance would still move me up, but using rotated(rotation) it causes that Vector2.UP to equal Vector2.RIGHT.

1 Like

However, for TNT what you could do is create a TNT node that stores its blast radius.

There might be simpler ways to do it, but if you have a var explosion_size: int #how many tiles away the tnt destroys.

func explode() -> Array:
var destroyed_tiles: Array
for x in explosion_size:
     for y in explosion_size:
     #calculate vector coords for each tile in explosion size
1 Like

Wow thank you so much for getting back to me. Yes I deleted because I didn’t want to bother you sorry about that. I had managed to get your script also working with an area2d, so it removed the box at the position of the area and I thought I was onto something. I was just gonna put a bunch of those areas together, but it would only work for the one spot. I thought if I somehow made 15 unique instances or instantiated a scene with a bunch of the exploded areas I could make it work but I realized I’m in way over my head lmao and I was about to hang it up but you just made my day.

And I really appreciate your solution for the TNT as well. Going to try putting that in ASAP and playing with it.

Omg, the way you’re explaining it is making it all click so thanks for that also. I see what you mean that the default vector is just up, that still is a little mystifying as to exactly why, but that concept is something I can understand because any input will be UP and the .rotated is going to rotate that vector by whatever value is in () which is the variable that you got from the object I think.

I was over here thinking the UP vector was literally ^^^. Idk if I have just a ton of brain fog and missed that explanation before but I appreciate you reiterating it here. Before I was able to follow the ball around connecting the signals, (still mystified about connecting the character node to the map node through the level node but I can at least not be completely baffled by I get that they are both children and being connected there at ready), but I constantly was getting stuck at the Vector2.UP and thinking that was going up and the drill distance was stopping it at the tip of the drill when in reality, from what I think you said is that drill_distance is just the distance from the object, drill in this instance, and that value is being moved and then rotated. Thank you a ton.

I apologize I have another question slog, also trying to layout possible next steps. No worries if you are unable to read through this, you have been tremendous help.

I can’t wrap my head around your TNT solution. For the explode function, is the Array the tilemap? And then when you say vector coords, are they coordinates of the tiles in the Array? And then have separate logic whichs sets the Array to be the tiles that are colliding with this collisionshape?

I think I saw something before about getting the colliders and storing them in an array but it was for inventory management. For now I think I’m going to try making the explosion a characterbody and making a collision shape be a big circle and see if I can get the colliders and see if it is just one collider or ones on the rim or if it’s all of them and somehow putting them in the Array but I am still unsure if this is the right move to start and also how to get the vectore coords for the tiles within the radius.

Thanks for your help going to try with this!

So, I woll touch on most present confusion. The Array will be an array that you add the Vectors from the explosion to.

So, it was a quick example mockup, there are other needed things. Eventually you’ll need to use that Array to calculate tiles to destroy.

for x in exp_size:
     for y in exp_size:
          destroy_tile.append(Vector2i(x,y)
tile_destruction.emit(destroy_tile, global_position)
queue_free()

One way would be use a signal to connect tnt and map, like you have with drill. Once you populate your array emit a signal at the end with that array and tnt global_position. In the func that recieves that array in your map convert the tnt global_position to map, then using that coord + the array cycle through your array.

func destroy_tile(tiles, origin):
var map_origin = local_to_map(to_local(origin))
for cell in tiles:
     #map_origin + cell is used to destroy tiles
1 Like