2D Damage knockback

Godot Version

Godot 4.3

Question

Hello! I have recently started godot and was testing some enemies. I wanted to add a knock back whenever the enemy hits the player. My current knock back system is very inconsistent. Sometimes it works and sometimes it doesn’t. During various interaction the knock back speed also changes.

extends Area2D

@onready var timer: Timer = $Timer
var damage: int
var knockbackDirection: int
@export var knockbackSpeed: int = 200

func _on_body_entered(body: Node2D) -> void:
	var playerHP = body.playerHurt(damage)
	if body.is_on_floor():
		body.velocity.y = -100
	body.velocity.x = knockbackDirection * knockbackSpeed
	print("New HP: " + str(playerHP))
	SoundManager.play_sound("hurt_sound")
	if playerHP <= 0:
		body.playerDied()
		Engine.time_scale = 0.5
		timer.start()

func _on_timer_timeout() -> void:
	Engine.time_scale = 1
	get_tree().reload_current_scene()

Hi, I assume this script is for a “hitbox” for an enemy that applies knockback to the player, right?

I notice that you don’t set knockbackDirection anywhere in the shown code. Where do you set it?

yes this is the hitbox code

the same hitbox is attached to various weapons like arrows and swords. The direction these weapons are facing is decided during runtime so the direction is assigned during runtime itself. I can show a snippet for more clarification

if animated_sprite_2d.flip_h:
		hurtzone.knockbackDirection = -1
		if hurtzone.get_node("CollisionShape2D").position.x > 0:
			hurtzone.get_node("CollisionShape2D").position.x *= -1
	else:
		hurtzone.knockbackDirection = 1
		if hurtzone.get_node("CollisionShape2D").position.x < 0:
			hurtzone.get_node("CollisionShape2D").position.x *= -1

here the direction is set based on the direction the enemy is facing at the moment.

When is this code called? Like, in _physics_process()?

its called in _process()

I don’t see anything wrong here. Is it possible that the code for moving the player will cancel the knockback somehow?

So, I have some thoughts.

First, I’d recommend taking almost everything in your knockback Area2D and move it to the player. I see how it would seem like it makes sense, but objects should be in charge of themselves, and the enemy should not need to know all about the player’s internal functions.

Second, I’d recommend showing us the full code of the Area2D and the Player, or you’re going to continue to get piecemeal answers and questions.

yeah i think this is one of the things thats happening since if i stay still and let the enemy hit me, the knock back works fine. But if i am moving, my character only jumps. Also i think the momentum from the projectiles is somehow being carried over since it just blasts me off

i switched the knowckback to the player script. Also i am attaching all the relevant scripts.

Hitbox/Hurtzone:

extends Area2D

@onready var timer: Timer = $Timer
var damage: int
var knockbackDirection: int
@export var knockbackSpeed: int = 200

func _on_body_entered(body: Node2D) -> void:
	var playerHP = body.playerHurt(damage, knockbackDirection)
	print("New HP: " + str(playerHP))
	SoundManager.play_sound("hurt_sound")
	if playerHP <= 0:
		body.playerDied()
		Engine.time_scale = 0.5
		timer.start()

func _on_timer_timeout() -> void:
	Engine.time_scale = 1
	get_tree().reload_current_scene()

Player:

extends CharacterBody2D

var playerHP = 10

@export var SPEED = 100.0
@export var deacceleration = 0.1
@export var JUMP_VELOCITY = -300.0
@export var rollSpeed = 70
var dead: bool = false
var jumping: bool = false
var jumpCounter: int = 0
var rolling: bool = false
var rollDir = 0

@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var buffer_timer: Timer = $RollBufferTimer
@onready var roll_wait_timer: Timer = $RollWaitTimer
@onready var jump_buffer_timer: Timer = $JumpBufferTimer

var canMove = true

func playerHurt(damage: int, direction: int):
	print("Old player HP: " + str(playerHP))
	playerHP -= damage
	if is_on_floor():
		velocity.y = -100
	velocity.x = direction * 200
	return playerHP

func playerDied():
	dead = true
	
func roll(direction):
	rolling = true
	canMove = false
	rollDir = sign(direction)
	if direction < 0:
		animated_sprite_2d.flip_h = true
	elif direction > 0:
		animated_sprite_2d.flip_h = false
	roll_wait_timer.start()
	buffer_timer.stop()
	
@warning_ignore("shadowed_variable_base_class")
func customGravity(velocity: Vector2):
	return get_gravity() if velocity.y < 0.0 else (get_gravity()*1.5)

func _physics_process(delta: float) -> void:
	if dead:
		animated_sprite_2d.play("hurt")
	else:
		# Add the gravity.
		if not is_on_floor():
			velocity += customGravity(velocity) * delta

		#Reset the jump counter
		if is_on_floor() and jumpCounter != 0:
			jumpCounter = 0

		# Handle jump.
		if Input.is_action_just_released("jump") and velocity.y < 0.0:
			velocity.y = JUMP_VELOCITY/3
		
		if Input.is_action_just_pressed("jump") and not rolling:
			jumping = true
			jump_buffer_timer.start()
		
		if jumping and not jump_buffer_timer.is_stopped() and jumpCounter < 2:
			SoundManager.play_sound("jump_sound")
			velocity.y = JUMP_VELOCITY
			jumpCounter += 1
			jumping = false
			
		if Input.is_action_just_pressed("roll") and is_on_floor() and not rolling:
			buffer_timer.start()
			
		if is_on_floor() and not buffer_timer.is_stopped():
			var rollDirection = Input.get_axis("move_left", "move_right")
			if rollDirection != 0:
				roll(rollDirection)
	
		# Get the input direction and handle the movement/deceleration.
		# As good practice, you should replace UI actions with custom gameplay actions.
		
		#Get direction: -1, 0, 1
		var direction := Input.get_axis("move_left", "move_right")
		
		if direction > 0:
			animated_sprite_2d.flip_h = false
		elif direction < 0:
			animated_sprite_2d.flip_h = true
			
		if rolling:
			animated_sprite_2d.play("roll")
		elif is_on_floor():
			if direction == 0:
				animated_sprite_2d.play("idle")
			else:
				animated_sprite_2d.play("run")
		else:
			animated_sprite_2d.play("jump")

		if direction and canMove and not rolling:
			velocity.x = direction * SPEED
		elif rolling and canMove:
			velocity.x = rollDir * rollSpeed
		elif not direction and is_on_floor():
			velocity.x = move_toward(velocity.x, 0, SPEED)

		move_and_slide()

func _on_animated_sprite_2d_animation_finished() -> void:
	rolling = false

func _on_roll_wait_timer_timeout() -> void:
	canMove = true

Arrow:

extends Area2D

@export var arrowSpeed = 110
@onready var hurtzone: Area2D = $Sprite2D/Hurtzone
@onready var sprite_2d: Sprite2D = $Sprite2D
var direction: int

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	hurtzone.damage = 1
	hurtzone.knockbackDirection = direction
	position.x += arrowSpeed * delta
	
func _on_visible_on_screen_notifier_2d_screen_exited() -> void:
	queue_free()

func _on_hurtzone_body_entered(body: Node2D) -> void:
	sprite_2d.visible = false
	set_process(false)
	hurtzone.set_deferred("monitoring", false)

Soldier/Enemy

extends Area2D

var direction = 1
var action = false
var rng = RandomNumberGenerator.new()

@onready var ray_cast_left: RayCast2D = $CollisionShape2D/RayCastLeft
@onready var ray_cast_right: RayCast2D = $CollisionShape2D/RayCastRight
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var arrow_placement: CollisionShape2D = $ArrowPlacement
@onready var Arrow = preload("res://scenes/arrow.tscn")
@onready var hurtzone: Area2D = $Hurtzone

func slashPlayer(playerDirection):
	action = true
	direction = playerDirection
	if direction < 0:
		animated_sprite_2d.flip_h = true
	else:
		animated_sprite_2d.flip_h = false
	var randomSlashPattern = rng.randf()
	if randomSlashPattern > 0.5:
		animated_sprite_2d.play("slash1")
		hurtzone.damage = 1
	else:
		animated_sprite_2d.play("slash2")
		hurtzone.damage = 2
	await get_tree().create_timer(0.4).timeout
	
	hurtzone.monitoring = true
	if animated_sprite_2d.flip_h:
		hurtzone.knockbackDirection = -1
		if hurtzone.get_node("CollisionShape2D").position.x > 0:
			hurtzone.get_node("CollisionShape2D").position.x *= -1
	else:
		hurtzone.knockbackDirection = 1
		if hurtzone.get_node("CollisionShape2D").position.x < 0:
			hurtzone.get_node("CollisionShape2D").position.x *= -1
	await get_tree().create_timer(0.1).timeout
	hurtzone.monitoring = false
	
	await animated_sprite_2d.animation_finished
	action = false
	animated_sprite_2d.play("idle")

func shootPlayer(playerDirection):
	action = true
	direction = playerDirection
	if direction < 0:
		animated_sprite_2d.flip_h = true
	else:
		animated_sprite_2d.flip_h = false
	animated_sprite_2d.play("shoot")
	await get_tree().create_timer(0.7).timeout
	
	var arrow = Arrow.instantiate()
	var arrowSprite = arrow.get_node("Sprite2D")
	if animated_sprite_2d.flip_h:
		arrowSprite.flip_h = true
		arrow.direction = -1
		arrow.arrowSpeed = arrow.arrowSpeed * -1
		if arrow_placement.position.x > 0:
			arrow_placement.position.x *= -1
	else:
		arrowSprite.flip_h = false
		arrow.arrowSpeed = arrow.arrowSpeed * 1
		arrow.direction = 1
		if arrow_placement.position.x < 0:
			arrow_placement.position.x *= -1
	arrow.global_position = arrow_placement.global_position
	
	add_sibling(arrow)
	await animated_sprite_2d.animation_finished
	action = false
	animated_sprite_2d.play("idle")

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	if not action:
		if ray_cast_left.is_colliding():
			if (position.x - ray_cast_left.get_collider().position.x) < 34:
				slashPlayer(-1)
			else:
				shootPlayer(-1)
		elif ray_cast_right.is_colliding():
			if (ray_cast_right.get_collider().position.x - position.x) < 13:
				slashPlayer(1)
			else:
				shootPlayer(1)

Hurtzone Nodes: Timer

Arrow Nodes: (Sprite2D → Hurtzone → CollisionShape2D)

Soldier Nodes: Animated Sprite2D, (ColliisionShape2D→RaycastLeft, RayCastRight), ArrowPlacement, Hurtzone→CollisionShape2D

I hope this info is enough. I have shown the nodes too. → denotes a subnode

		if direction and canMove and not rolling:
			velocity.x = direction * SPEED
		elif rolling and canMove:
			velocity.x = rollDir * rollSpeed
		elif not direction and is_on_floor():
			velocity.x = move_toward(velocity.x, 0, SPEED)

I believe that this code will override the knockback velocity, since there is no check if the player is being knocked back. Maybe you could set canMove to false when the player is knocked back, or you can set up a state machine for the player.

1 Like

i tried doing canMove to falsebut since both of them are being changed in the same function, it doesn’t work well. I have no idea what a state machine is so I will learn about it now.

Apart from all this, I am trying some other stuff to let the player system know that they are currently being knockbacked, so that the normal velocity doesn’t override the knockback velocity. I will also definitely checkout state machines.

1 Like

You can check out my State Machine Plugin. It comes with instructions. It is open source and free.

1 Like

Thank you! Will definitely do that

1 Like