Enemy Drops Not Updating Collision When Instanced Right on the Player

Godot Version

4.3

Question

In my game I’ve set up enemies that are able to drop items upon death. They are instanced as Area2D’s and added as new children to the parent scene. However, sometimes when the drops spawn in, they spawn directly on the player, and the collisions do not update (the player has to leave the drop hitbox and run back into it). How can I make it so when the drop is instanced onto the player it will update and be able to be picked up right then and there?

Code for drops:

public partial class Collectable : Area2D
{
	[Export] private InventoryItem item;
	[Signal] public delegate void PlayerCollectEventHandler(InventoryItem item);
	private player player;
	private bool onCooldown = true;
	public override void _Ready()
	{
		player = GetNode<player>("/root/Player");
	}

	private void OnBodyEntered(Node2D body)
	{
		if (onCooldown == false)
		{
			if (body.IsInGroup("Player"))
			{
				player = (player)body;
				PlayerCollecting();
			}
		}
	}

	private void PlayerCollecting()
	{
		player.Collect(item);
		QueueFree();
	}

	private void OnTimerTimeout()
	{
		onCooldown = false;
		GD.Print("timer off");
	}

Enemy code to create instances:

private void DropItem()
	{
		var drop = (Area2D)itemDrop.Instantiate();
		drop.GlobalPosition = new Vector2((float)GD.RandRange(Position.X - 15, Position.X + 15), (float)GD.RandRange(Position.Y - 15, Position.Y + 15));
		GetParent().AddChild(drop);
	}

I hope there is a far more easier answer to this but have you tried to use one of the Area2D methods to check that programmatically?

Out of curiosity: Why has an item collect feature a cooldown? Maybe the cooldown timer has not been reset correctly?


Btw. unrelated, but there is a way to flatten your indentation in your code in situations like these (see Early Return Pattern in C#):

private void OnBodyEntered(Node2D body)
{
	if (onCooldown || !body.IsInGroup("Player"))
	{
		return;
	}

	player = (player)body;
	PlayerCollecting();
}

This may be “bad form”, and I honestly don’t know if it will work (I don’t know if body_entered() is commutative).

But the solution/idea I have would be to on ready() for the spawned/dropped item, set its position to (0,0) or somewhere far outside the bounds of the level/camera, then immediately set its position back to its initial position. Similar to the old dev trick of pre-loading objects and hiding them beneath the floor.

In theory, if body_entered() emits when it enters another object while being the entity that changed position (commutative, a entering b == b entering a), then this should trigger body_entered, which should trigger the pickup.

Again, sorry if this isn’t what you’re looking for, as it is more of a hacky solution than a technical one.

1 Like

Area2D only work when something entering collision and not checking if there is something inside on spawn.
My solution will be to use Shape cast from code on enter tree or ready. node shape cast can work too, but is more heavy than Area2D.

in rare situation our player can be to near 0,0.

1 Like

Reading the documentation for ShapeCast, it sounds like an excellent solution without my hacky-nonsense.

1 Like

Area2D node generally don’t emit the entered signal for body/areas already inside them (sometimes work, sometimes not, so is not a good way to do) also in your case even if the signal has emmited, will not work because you block the collect for a time, so the signal will be emmited but onCooldown will be true and block the PlayerCollecting() call. For this case you need to do a manual check using Area2D.get_overlapping_bodies()

public partial class Collectable : Area2D
{
	[Export] private InventoryItem item;
	[Signal] public delegate void PlayerCollectEventHandler(InventoryItem item);
	private player player;
	private bool onCooldown = true;
	public override void _Ready()
	{
		player = GetNode<player>("/root/Player");
	}

	private void OnBodyEntered(Node2D body)
	{
		if (onCooldown == false)
		{
			if (body.IsInGroup("Player"))
			{
				player = (player)body;
				PlayerCollecting();
			}
		}
	}

	private void PlayerCollecting()
	{
		player.Collect(item);
		QueueFree();
	}

	private void OnTimerTimeout()
	{
		onCooldown = false;
		GD.Print("timer off");

		# I'll write using GDScript because i'm not familiar with C#
		# so you can covert later
		await get_tree().physics_frame
		await get_tree().physics_frame
	
		for body in get_overlapping_bodies():
			emit_signal("body_entered", body)
	}
2 Likes

Custom Shape cast can look like this:

	public override void _Ready()
	{
		var result = ShapeCast();
		string message = "Shape Collide with: ";
		Node2D collider;
		for (int i = 0; i < result.Count; i++)
		{
			collider = (Node2D)result[i]["collider"];
			message += collider.Name + ", ";
		}

		GD.Print(message);
	}
	private Godot.Collections.Array<Godot.Collections.Dictionary> ShapeCast()
	{
		var collisionShape2D = GetNode<CollisionShape2D>("CollisionShape2D");
		var query = new PhysicsShapeQueryParameters2D
		{
			Shape = collisionShape2D.Shape,
			Transform = collisionShape2D.GlobalTransform,
			CollideWithAreas = false,
			Exclude = [GetRid()]
		};
		return GetWorld2D().DirectSpaceState.IntersectShape(query);
	}

Thanks for the solution! I remixed it just a bit but ultimately this was it :slight_smile:

Yep, using the GetOverlappingBodies method ended up working out. To answer your question, I’m not sure how familiar you are with Minecraft’s item drop system, but basically there is a slight delay before it is picked up which I’m assuming is done so the player can see that they are actually getting something. If it happened instantly it would be easy to miss. It also gives me room in case I decide to add a drop animation.

This definitely is something I would have considered, but I preferred using something less hacky as you explained. Thank you regardless.

1 Like

I’m not comfortable with using custom ShapeCasts and I didn’t want to just copypaste the code, so I’d have to do more research on this first before I work with it. Thank you though.

PhysicsShapeQueryParameters2D here you configure.

await in C# work different. We needed await ToSignal and method with await needed be async private void OnTimerTimeout() like this:
private async void OnTimerTimeout()
ToSignal like this:

		await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame);
		await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame);

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.