here is the basic flow for knock back system. I’ll share the whole script as well, it’ll be a bit messy so here is the flow for your understanding. (also know that this may not be the efficient but this worked for me.)
knock back for enemies:
I have like 3 files or references for the knock back.
- enemy_base.gd : this will be common file for enemies will have some common logic for things like enemy health, death, sprite flip and some common behaveiors like knockback etc.
about the knock back logic, I’ve defined some variables related to knock back.
# knockback related variables
var is_knocked_back: bool = false
var knockback_duration: float = 0.2
var knockback_timer: float = 0.0
var knockback_force: Vector2 = Vector2(200, 0)
after this whenever your want to apply the knockback you set an is_knocked_back flag to true and set the knockback_duration (this define how much longer force should apply), get the knockback direction and add velocity in opposite direction (you’ll need the get player position for this. im calling damage function form the player script via area2d_body_entered signal and passing the player position in it)
after done setting the values now we need a logic to change the is_knocked_back flage again. that logic is there in second function. (you can experiment it by removing the timer completely and apply force one time only.)
# common function for all enemies
func apply_knockback(hit_from: Vector2) -> void:
is_knocked_back = true
knockback_timer = knockback_duration
var knockback_dir = (global_position - hit_from).normalized()
velocity = knockback_dir * knockback_force
# common function for all enemies
func knockback_logic(delta: float) -> void:
knockback_timer -= delta
if knockback_timer <= 0:
is_knocked_back = false
move_and_slide()
func apply_damage(hit_from: Vector2, damage: int) -> void:
# push the enemy backwards
apply_knockback(hit_from)
# reduce the health
enemy_health -= damage
# update the visuals (use teh blink shader)
var tween = self.create_tween()
tween.tween_method(setShader_BlinkIntensity, 1.0, 0.0, 0.5)
# if health reaches 0 remove the enemy
death()
this was the main logic for knockback, now we just need to club this together and take the necessary input form other nodes.
- enemy_script : I have multiple enemy scripts which are inheriting the common behavior from enemy_base script. (depending on your scope and requirement you can skip this step and have enemy behaviour in one file only).
in this file im actively checking if enemy is in knockback state (is_knocked_back flag) if so then call the knockback_logic funcion
func _physics_process(delta: float) -> void:
if is_knocked_back:
knockback_logic(delta)
return
# rest of the code
- player.gd: I wanted to apply the knock back when player will hit the target and in direction opposit to the player so we need player position for that im calling the damage enemy function via area2d_body_entered signal and passing player position and attack power into it. global position will be used for knockback logic and attack power will be used for reducing the enemy health.
func _on_attack_area_body_entered(body: Node2D) -> void:
print(body.name , " is attacked")
if body.is_in_group('enemies'):
if Input.is_action_pressed("look_down"):
pogo_jump()
body.apply_damage(global_position, ATTACK_POWER)
below is the whole script for your reference (I tried uploading the zip file but it did not work so sharing he it like this)
enemy_base.gd :
extends CharacterBody2D
# common variables
var speed: int = 200
var dir: int = 1
var enemy_health: int = 10
# patrol related variables
var patrol_timer: float = 0.0
var patrol_start_pos: Vector2
var patrol_end_pos: Vector2
var delay_at_end: float = 0.5
var move_direction: Vector2
# knockback related variables
var is_knocked_back: bool = false
var knockback_duration: float = 0.2
var knockback_timer: float = 0.0
var knockback_force: Vector2 = Vector2(200, 0)
# attack related variables
var can_attack: bool = true
# reference variables
var enemy_sprite: Sprite2D
var enemy_animation: AnimationPlayer
var detection_range: Area2D
var damage_area: Area2D
var player: CharacterBody2D
var projectile: PackedScene
var attack_cooldown: Timer
func patrol(delta: float) -> void:
# stop for a moment when reached at end of patrol point
if patrol_timer > 0:
patrol_timer -= delta
velocity.x = 0
return
# switch target pos based on dir
var target_pos: Vector2 = patrol_end_pos if dir == 1 else patrol_start_pos
# switch the dir and start the delay timer when reached at one of the end points
if global_position.distance_to(target_pos) < 5:
dir *= -1
patrol_timer = delay_at_end
move_direction = (target_pos - global_position).normalized()
# below is the move behaviour(for now it just straight line movment)
patrol_pattern()
# the following function can be overridden for differnt patroll behaviours
func patrol_pattern() -> void:
velocity.x = speed * move_direction.x
func chase(player_pos: Vector2, speed_multiple: float = 2) -> void:
# get the player direction and execute the chase behaviour
var player_direction = (player_pos - global_position).normalized()
# below is the chase behaviur(for now it just straight line movment)
velocity.x = player_direction.x * speed * speed_multiple
func shoot_projectile(projectile_type: int = 1) -> void:
# set can_shoot flag to false so that enemy will not be able to spam projectiles
can_attack = false
# create the instance of the projectile and add it to the parent scene
var projectile_instance = projectile.instantiate()
projectile_instance.global_position = position
# below is the projectile set up specific for projectile type. logic is in the projectile script
# projectile type : {1: straight line, 2: tracking player}
projectile_instance.type = projectile_type
# following line is for straight projectile
projectile_instance.dir = Vector2(dir,0)
# followwin line is for traking player
projectile_instance.target_body = player
get_parent().add_child(projectile_instance)
# start the cooldown timer, when the timer is up enemy can shoot again
attack_cooldown.start()
# common function for all enemies
func apply_damage(hit_from: Vector2, damage: int) -> void:
# push the enemy backwards
apply_knockback(hit_from)
# reduce the health
enemy_health -= damage
# update the visuals (use teh blink shader)
var tween = self.create_tween()
tween.tween_method(setShader_BlinkIntensity, 1.0, 0.0, 0.5)
# if health reaches 0 remove the enemy
death()
# common function for all enemies
func apply_knockback(hit_from: Vector2) -> void:
is_knocked_back = true
knockback_timer = knockback_duration
var knockback_dir = (global_position - hit_from).normalized()
velocity = knockback_dir * knockback_force
# common function for all enemies
func knockback_logic(delta: float) -> void:
knockback_timer -= delta
if knockback_timer <= 0:
is_knocked_back = false
move_and_slide()
# common function for all
func death() -> void:
# this is just a place holeder func (update this when animations are added)
if enemy_health <= 0:
queue_free()
# common function for all enemies
func setShader_BlinkIntensity(newValue: float) -> void:
enemy_sprite.material.set_shader_parameter("blink_intensity", newValue)
# common function for all enemies
func flip_enemy() -> void:
if velocity.x != 0:
enemy_sprite.flip_h = velocity.x < 0
# remove the below code if enemies do not have the detection_range as area2d node
if detection_range:
if velocity.x > 0:
detection_range.scale.x = 1
elif velocity.x < 0:
detection_range.scale.x = -1
#----------------------------------SIGNALS---------------------------------------------
# common signal for all the enemies
# player gets hurt when touched by enemy
func _on_damage_area_body_entered(body: Node2D) -> void:
if body.is_in_group('players'):
body.take_damage(global_position)
# common signal for chase logic
func _on_detection_range_body_entered(body: Node2D) -> void:
if body.is_in_group('players'):
player = body
# common signal for chase logic
func _on_detection_range_body_exited(body: Node2D) -> void:
if body.is_in_group('players'):
player = null
func _on_attack_cooldown_timeout() -> void:
can_attack = true
petrolling_enemy.gd :
extends "res://scripts/enemies/enemy_base.gd"
@export var PATROL_DIST: int = 128
func _ready() -> void:
# add enemy to the group and setup nessery nodes
add_to_group('enemies')
enemy_sprite = $Sprite2D
damage_area = $damage_area
# calculate and store patrol points
patrol_start_pos = Vector2(global_position.x - PATROL_DIST, global_position.y)
patrol_end_pos = Vector2(global_position.x + PATROL_DIST, global_position.y)
# connet the signals to the base class methoths(can use the custom signals too)
damage_area.body_entered.connect(_on_damage_area_body_entered)
func _physics_process(delta: float) -> void:
if is_knocked_back:
knockback_logic(delta)
return
patrol(delta)
flip_enemy()
move_and_slide()
Player.gd :
extends CharacterBody2D
# define some constants params (i'll be using var for development purpose)
@export var SPEED: int = 300
@export var ACCELERATION: int = 900
@export var FRICTION: int = 1800
@export var GRAVITY: int = 980
@export var ATTACK_POWER: int = 2
# jump params
@export var JUMP_FORCE: int = 800
@export var coyote_time: float = 0.1
@export var jump_buffer_time: float = 0.1
var coyote_timer: float = 0.0
var jump_buffer_timer: float = 0.0
var can_double_jump: bool = true
# dash params
@export var DASH_SPEED: int = 900
@export var DASH_DURATION: float = 0.2
@export var DASH_COOLDOWN: float = 1.0
var is_dashing: bool = false
var can_dash: bool = true
var dash_timer: float = 0.0
var dash_cooldown_timer: float = 0.0
var dash_direcction: int = 1
#knockback params
@export var KNOCKBACK_FORCE: Vector2 = Vector2(0,-400)
@export var INVINCIBILITY_DURATION: float = 1.0
var is_invincible: bool = false
#attack params
@export var ATTACK_COOLDOWN: float = 0.3
var is_attacking: bool = false
var can_attack: bool = true
#player states
enum State {IDLE, RUN, JUMP, FALL}
var curr_state = State.IDLE
#nodre references
var player_sprite: Sprite2D
var player_animation: AnimationPlayer
var attack_area: Area2D
var weapon_sprite: AnimatedSprite2D
func _ready() -> void:
add_to_group('players')
player_sprite = $Sprite2D
player_animation = $AnimationPlayer
attack_area = $attack_area
weapon_sprite = $attack_area/AnimatedSprite2D
weapon_sprite.visible = false
func _physics_process(delta: float) -> void:
var direction: int = Input.get_axis("move_left", "move_right")
horizontal_movment(direction, delta)
jump(delta)
dash(delta)
attack()
apply_gravity(delta)
handle_flip()
handle_state_logic()
move_and_slide()
func horizontal_movment(direction: int, delta: float) -> void:
# horizontal movment with firction and acceleration
if direction:
velocity.x = move_toward(velocity.x, SPEED * direction, ACCELERATION * delta)
else:
velocity.x = move_toward(velocity.x, 0, FRICTION * delta)
func apply_gravity(delta: float) -> void:
# apply gravity
if !is_on_floor():
var fall_multiplyer: float = 2 if velocity.y > 0 else 1
#velocity.y += GRAVITY * delta * fall_multiplyer
velocity.y = move_toward(velocity.y, GRAVITY , 1500 * delta * fall_multiplyer)
coyote_timer -= delta #gradually decrease coyote timer when not on floor
#velocity.y += clamp(velocity.y, 0, 1500)
else:
coyote_timer = coyote_time #reset coyote timer when on the floor
can_double_jump = true
func jump(delta: float) -> void:
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = jump_buffer_time #on jump input start jump buffer time to record the jump for fraction of second
#double jump logic
if can_double_jump and !is_on_floor(): #while in air check if is able to jump again
#velocity.y = 0
velocity.y = -JUMP_FORCE #if is able to jump, change the y height
if coyote_timer < 0 : #make sure to check coyote timer first before deactivating double jump
can_double_jump = false
else:
jump_buffer_timer -= delta
# jump buffer logic (incase player tries to jump just before touching or leaving the floor)
if jump_buffer_timer > 0 and coyote_timer > 0:
velocity.y = -JUMP_FORCE
jump_buffer_timer = 0.0
coyote_timer = 0.0
# variable jump height logic
if Input.is_action_just_released("jump") and velocity.y < 0:
velocity.y /= 5 #the value 5 is just for experimental purpose
func dash(delta: float) -> void:
if Input.is_action_just_pressed("dash") and can_dash and !is_dashing:
#setup some dahs flags and variables
is_dashing = true
can_dash = false
dash_timer = DASH_DURATION
dash_cooldown_timer = DASH_COOLDOWN
dash_direcction = -1 if player_sprite.flip_h else 1 # set dash direction based on flip_h property of sprite
velocity.x = DASH_SPEED * dash_direcction
#here (need to add) the code to change the collision layers and mask
if !can_dash:
dash_cooldown_timer -= delta
if dash_cooldown_timer <= 0:
can_dash = true
if is_dashing:
velocity.y *= 0.1
dash_timer -= delta
if dash_timer < 0:
#velocity.x = 0
is_dashing = false
func attack() -> void:
if Input.is_action_just_pressed("attack") and can_attack:
#attack flags
weapon_sprite.visible = true
weapon_sprite.play('slash')
is_attacking = true
can_attack = false
attack_area.set_deferred('monitoring', true)
#playe animation here
#below code will go into attack animation finish func
await get_tree().create_timer(ATTACK_COOLDOWN).timeout
can_attack = true
is_attacking = false
attack_area.set_deferred('monitoring', false)
weapon_sprite.visible = false
func take_damage(hit_source: Vector2) -> void:
if is_invincible:
# skip if already invincible
return
# apply knockback in opp direction
var knockback_direction = sign(global_position.x - hit_source.x)
velocity.x = knockback_direction * KNOCKBACK_FORCE.x
velocity.y = KNOCKBACK_FORCE.y
# start invincibility state
is_invincible = true
start_blinking()
await get_tree().create_timer(INVINCIBILITY_DURATION).timeout
is_invincible = false
player_sprite.modulate.a = 1
func start_blinking() -> void:
var blink_timer = INVINCIBILITY_DURATION/10
for i in range(5):
player_sprite.modulate.a = 0.5 if i % 2 == 0 else 0.8
await get_tree().create_timer(blink_timer).timeout
func handle_flip() -> void:
var flip_factor: int = 1
if velocity.x != 0:
player_sprite.flip_h = velocity.x < 0
if player_sprite.flip_h:
attack_area.scale.x = -1
flip_factor = 1
else:
attack_area.scale.x = 1
flip_factor = -1
if Input.is_action_pressed("look_up"):
attack_area.rotation_degrees = 90 * flip_factor
elif Input.is_action_pressed("look_down"):
attack_area.rotation_degrees = -90 * flip_factor
else:
attack_area.rotation_degrees = 0
func pogo_jump(pogo_multiple: float = 1) -> void:
print("pogo activated")
#velocity.y = 0
velocity.y = -JUMP_FORCE * pogo_multiple
can_double_jump = true
func change_state(new_state: State) -> void:
if curr_state == new_state:
return
curr_state = new_state
update_animations(curr_state)
func update_animations(state: State) -> void:
var anim_name = ''
match state:
State.IDLE:
anim_name = 'dash'
State.RUN:
anim_name = 'run'
State.FALL:
anim_name = 'fall'
State.JUMP:
anim_name = 'jump'
if player_animation.name != anim_name:
player_animation.play(anim_name)
func handle_state_logic() -> void:
if is_on_floor():
if velocity.x != 0:
change_state(State.RUN)
else:
change_state(State.IDLE)
else:
if velocity.y > 0:
change_state(State.FALL)
else:
change_state(State.JUMP)
func _on_attack_area_body_entered(body: Node2D) -> void:
print(body.name , " is attacked")
if body.is_in_group('enemies'):
if Input.is_action_pressed("look_down"):
pogo_jump()
body.apply_damage(global_position, ATTACK_POWER)
Hope this helps. also about the health. I went through your devlog and it appeared like you may have already implemented it. so i haven’t added that code. if you required that too feel free to ask (to be honest my own health system was just the dummy so it may not be of that much help)