How to do enemy knockback in a 3D game?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Zilva

I wanted to add a knock back effect on my melee weapons in the FPS I’m making.
I’ve tried to make it work by modifying the target speed, but that doesn’t work. They don’t arc, land in an unnatrual way, and get a speed boost towards you on the first frame of getting hit. Then I tried affecting their velocity, but outside of being able to zero it, I didn’t manage to make a lasting impact that way. I can’t even get gravity to work with enemies, though it does work on the player.

It’s gotten to the point where I can’t even precisely describe what my problem is, as I’ve become completely exhausted and confused by my code. It’s a mess and a half.

The biggest issue is getting enemies to land. They don’t. Instead, they walk to the ground. Even if I restrict the Velocity.x and Velocity.z (vars referring to Vector3 of course) to 0 until is_on_floor() is true, the falling speed is abysmal and unrealistic unless I specifically increase it while in the air. This doesn’t work because I then can’t reset the speed, since resetting it using checks in delta causes them to reset every frame. However, to do so differently, I’d have to cook up a pot of spaghetti.

I’ve tried processing gravity the way I do it on the player, but it doesn’t affect the falling speed in the least. It’s nonsensical. With Vector.y = 20 and Gravity = -30 and a physics process as follows:

 function velocity.y += gravity * delta

My player controller allows for amazingly responsive landing, but for the enemy it’s like I haven’t told them to do anything.

I use basic kinematic body enemies, with no rag dolls. Their movement AI is the classic solution supposedly everybody uses:

move_and_slide(direction.normalized() * speed, Vector3.UP)

I beg you, write me a function that would knock back this target with the strength of a variable and explain how it works. I don’t have it in me to ask for simple advice on how to get there myself. It’s been three days. Apparently, I don’t understand anything about how Vector3 works.

Otherwise I’m gonna have to restrict the kinematic body on the Y axis and live with enemies that can’t move up ever.

For knockback You will have to apply new direction and speed until the ground is reached again. But for this to work, it cannot conflict with your general movement code. So your movement code must be prepared for eventuality of applying other forces than just input or AI. Paste full movement code of player and enemy

Inces | 2022-09-09 14:02

Enemy:

var velocity = Vector3()
var gravity = -30
var CanMove = true
var dying = false
        
func knockback(knockback):
	var current_speed = speed
	speed -= knockback
	$AnimatedSprite3D.play("stagger")
	yield($AnimatedSprite3D,"animation_finished")
	speed = current_speed

		
func _physics_process(delta):
	velocity.y += gravity * delta
	if path_index < path.size():
		var direction = (path[path_index] - global_transform.origin)
		if direction.length() < 1:
			path_index += 1
		else:
			if CanMove:
				$AnimatedSprite3D.play("walk")
				move_and_slide(direction * speed, Vector3.UP)

Player:

var velocity = Vector3()
var gravity = -30
var jump_speed = 20
var mouse_sensitivity = 0.002

func get_input(): #basic movement input
	var input_dir = Vector3()
	if Input.is_action_pressed("move_forward"):
		input_dir += -global_transform.basis.z
	if Input.is_action_pressed("move_back"):
		input_dir += global_transform.basis.z
	if Input.is_action_pressed("move_left"):
		input_dir += -global_transform.basis.x
	if Input.is_action_pressed("move_right"):
		input_dir += global_transform.basis.x
	input_dir = input_dir.normalized()
	return input_dir

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		rotate_y(-event.relative.x * mouse_sensitivity) #up and down
		$Pivot.rotate_x(-event.relative.y * mouse_sensitivity)
		$Pivot.rotation.x = clamp($Pivot.rotation.x,-1.2,1.2) 
		
func _physics_process(delta):
	velocity.y += gravity * delta
	var desired_velocity = get_input() * PlayerStats.Player_speed
	velocity.x = desired_velocity.x
	velocity.z = desired_velocity.z
	if Input.is_action_just_pressed("jump") and PlayerStats.jumpLimit > 0:
		PlayerStats.jumpLimit -= 1
		velocity.y = jump_speed
	if is_on_floor() and velocity.y <= 0:
		PlayerStats.jumpLimit = PlayerStats.max_jumps
	velocity = move_and_slide(velocity, Vector3.UP)

Most notably, the gravity variable doesn’t affect the AI. No matter what number I set it to, it always falls down at the same speed.

Zilva | 2022-09-10 10:07

:bust_in_silhouette: Reply From: Inces

As I thought your general movement code doesn’t allow any other source to interfere with movement. It only respects direction created by pathfinding. You need to loosen it there.
What about gravity - You made gravity affect velocity.y, but never used velocity to calculate actual movement :slight_smile: Add your gravity to 1st argument of move_and_slide, like + Vector2(0,gravity*delta)

direction should be a variable of highest scope, introduce it in the origin of script.
let move and slide be going on always, independently from canmove
If canmove is true - direction will be calculated from pathfinding and general speed will be used
if canmove is false, no calculations whatsoever
on knockback set canmove to false and only now You can freely change direction and speed as You see fit. You tried to do it with speed only, but You obviously need direction. On stagger finished - canmove is true again

Huge thanks for your response. It adds a lot of context to what I couldn’t understand.

I’ve spent the weekend watching even more guides on movement and explosive weapons to figure this out, and somehow made it functions. The code is very messy though, I didn’t think of adding a vector2, or that it’s even something I can use in a 3D game. I’ll try to optimize the code with it.

Right now, the updated code looks as follows:

In the enemy script:

func _physics_process(delta):
velocity.y += gravity * delta
knockback = knockback.move_toward(velocity, 200 * delta)
knockback = move_and_slide(knockback)
if path_index < path.size():
	var direction = (path[path_index] - global_transform.origin)
	if direction.length() < 1:
		path_index += 1
	else:
		if not is_on_floor():
			velocity = move_and_slide(velocity, Vector3.UP, true)
		if can_move and is_on_floor():
				$AnimatedSprite3D.play("walk")
				move_and_slide(direction.normalized() * speed, Vector3.UP)

func knockup(origin, strenght, knockup_power):
can_move = false
velocity.y *= -knockup_power
knockback(origin, strenght)
$AnimatedSprite3D.play("stagger")
yield($AnimatedSprite3D, "animation_finished")
can_move = true

func knockback(origin, strenght):
	knockback = origin * strenght

and then the weapon calculates the origin, which I’m not sure if it even works proper, to be honest. The end result is enemies flying back in an arc I can customize, so it’s mostly fine, I think.

func check_hit():
	var area = $Area
	var reach = area.get_overlapping_bodies()
	for body in reach:
		if body.is_in_group("Player"):
			continue
		if body.is_in_group("enemy"):
			var origin = (body.global_transform.origin - global_transform.origin).normalized()
			body.knockup(origin, knockback_power, knockup_power)
			body.take_damage(total_damage)
			var new_blood = blood.instance() #blood particle code starts
			body.add_child(new_blood)
			new_blood.emitting = true

Zilva | 2022-09-12 09:04