• 2D
  • Collision detection on KinematicBody2D side

I'm learning Godot right now and decided to create simple 2d game to get understanding of the engine. I'm currently designing damage system (e.g. when bullet or any other projectile hit enemy/player, there should be hit event which would trigger taking damage, reducing health bla-bla-bla) and encountered one issue. I thought that the most logical way to design such system would be to check for collision on enemy/player side. I put the bullet collision check in _physics_process movement function of enemy (which is KinematicBody2D class):

var collision = move_and_collide(-v*speed*delta);
if collision != null:
	if collision.collider.is_in_group('Projectile'):
		hit();

I was surprised when found out that move_and_collide function doesn't return any object when bullet hits enemy! Though, when enemy collides with player, the same code (with a slight change to check for Player group) works fine.

I read some answer that KinematicBody2D may not detect collision if some other object hits it. This would suggest to put such collision check to the bullet code instead. However, from my point of view that would be a bad design since bullet should care only about events which has impact on it (like bullet hit any object -> play hit sound -> disappear).

As another workaround I could add additional Area2D with another collision to the Enemy node that would specifically check for collisions with projectiles. However, this seems to me as even worse idea since in this case I have to make Area2D collision bigger that Enemy's shape collision in order to catch it. And this may also have impact on performance since engine will calculate 2 collisions separately.

Is there any other way to make the trick and call bullet collision check in enemy's script? Or engine doesn't have any out of the box solutions?

Thank you in advance!

a month later

If your bullet is an Area2D then the only way to detect collisions between the bullet and your player is with Area2D's body_entered signal. The easiest way would be to put the damaging code in your bullet. Something like this:

# Bullet.gd
func _ready():
	# connect the body_entered signal to ourself
	connect("body_entered", self, "_on_Bullet_body_entered")

func _on_Bullet_body_entered(body):
	# Check if body is a player or enemy
	if body.is_in_group("CanDamage"):
		body.damage(amount)

But, if you really want to keep your damage code in the player/enemy scripts, you can connect the body_entered signal to the player/enemy. This way the _on_Bullet_body_entered function will be in the player not the bullet:

# Bullet.gd
func _ready():
	# Loop through all players and enemies
	for character in get_tree().get_nodes_in_group("CanDamage"):
		# Connect the signal to the player/enemy instead of ourself.
		connect("body_entered", character, "_on_Bullet_body_entered")

# Player.gd and Enemy.gd
func _on_Bullet_body_entered(body):
	# Now that we connected the signal to the player/enemy, 
	# we can put the function inside our player / enemy script.

	# Don't forget to check if the body the bullet collided into is ourself, 
	# as this signal will still fire whenever the bullet collides into anything.
	if body == self:
		damage()

Welcome to the forums @Arakiti!

I would recommend using an Area2D around the enemies and players to detect bullets. While there is a slight performance cost for this (two collision checks instead of just one), unless you have many, many players and enemies all at once, it probably won't affect performance too badly that it would be noticeable. I've done this several times for both 2D and 3D and it has worked well. The Area2D node isn't nearly as performance heavy as the KinematicBody2D node, in my experience.

The benefit of doing it this way as well is that the bullet can just be a bullet, it doesn't need any extra collision or even need to know anything about what it collided with at all, making the code easier. You can optionally call functions on the object(s) that enter the area (check for functions using has_method, for example) if you need the bullet or other damaging object to have a reference to the player or thing it hit.

All in all, I would highly suggest using an Area2D. It is simpler to program, gives a lot of flexibility in what you can do with it, and it keeps the objects pretty separate from each other if you want. It doesn't work for every project, and as you mentioned it has a performance cost, but it is the solution I would try initially.


That said, there are some ways around it that might work. The first I can think of is to have your bullets use RigidBody2D nodes instead of KinematicBody2D nodes, as I believe KinematicBody2D node to RigidBody2D collision is a bit more stable because it's done by the physics engine. The minor problem here is that you may still miss collisions if the player/enemy KinematicBody2D body is moving towards a bullet that is moving towards them, as then the physics engine has to resolve who "collided" with whom in that case since they are both moving towards each other. I do not remember if I've tried this though, so I'm not 100% sure if it will work nor if you'd encounter issues with it or not.

Another, potentially lightweight solution would be to have the bullet's use a raycast instead of a KinematicBody2D. This would be a little more performant and would solve the issue of collisions being missed. What you'd want to do though is have the bullet send a function if it hits a damageable object, and then move in the direction of the raycast if it doesn't hit anything. The code for the bullet could look something like this:

extends Node2D

# I'll assume the velocity is set by whatever is firing the bullet
var velocity = Vector2.ZERO

# the amount of damage a bullet would do
var bullet_damage = 10

func _physics_process(delta):
	var space_state = get_world_2d().direct_space_state
	# raycast to where the bullet will be moving towards
	var raycast_result = space_state.intersect_ray(global_position, global_position + (velocity * delta))
	# if something was hit
	if raycast_result:
		if raycast_result.collider.has_method("damage"):
			raycast_result.collider.damage(bullet_damage)
		
		# free/destroy the bullet
		queue_free()