How could i know the correct left and right direction to AI of an enemy with a doom style game?

Godot Version

4.2.2

Question

I’m tried to follow this guide to make the 8 directional walking to my enemy:

The problem I noticed is that when I tried to implement the AI movement, the animation did not play correctly; for instance, the enemy plays the walking-left animation when it is moving to the right, as can be seen below:

I’ve tried to test the velocity of enemy, but i’m confused about which axis i need to consider because player can change it’s position, anyone could help me?

image
-x is considered left, but basis.x is pointing to the right so this should technically be right.

in other words your x axis is reversed.

these are the default values you can see that they point in the opposite direction initially.
Vector3d
image
basis
image

if that doesn’t fix it you will need to share code

Not works :S, it’s still moving in a direction that don’t match with animation, here is my code:

Sprite3D

extends EnemyTexture
class_name Skeleton_Texture

@onready var idle_dir: String = "idle"
@export var anim_col: int = 0

func animate(velocity: Vector3):
	if enemy.can_hit or enemy.can_attack or enemy.can_die:
		action_behavior()
	else:
		move_behavior(velocity)

func move_behavior(velocity: Vector3):
	if velocity.x != 0 and velocity.z != 0 and !(enemy.is_attacking or enemy.is_retreating):
		play_walk(velocity)
	else:
		animation.play(idle_dir)
	pass

func play_walk_bilboard_camera(_velocity: Vector3):
	var player: Player_Skeleton = get_tree().get_first_node_in_group("Player")
	var p_fwd = -player.camera_principal.global_transform.basis.z
	var fwd = enemy.global_transform.basis.z
	var left = -enemy.global_transform.basis.x
	print("global basis:", enemy.global_transform.basis)
	
	var l_dot: float = left.dot(p_fwd)
	var f_dot: float = fwd.dot(p_fwd)
	var row: int = 0
	#print("fdot: ",f_dot, " l_dot:", l_dot)
	#print("position diff: ", player.global_position.length() - enemy.global_position.length())
	if f_dot < -0.85:
		row = 0 #front sprite
	elif f_dot > 0.85:
		row = 3 #back sprite
	else:
		if l_dot > 0:
			row = 2 #right sprite
		elif abs(f_dot) < 0.3:
			row = 1 #left sprite
		elif f_dot < 0 :
			row = 1 #forward left sprite
		else:
			row = 3 #back sprite
			
	frame = anim_col + row * 9
	animation.play("walk_anim_col")
	
func play_walk(velocity: Vector3):
	#print("velocity x:", abs(velocity.x), "velocity z:", abs(velocity.z))
	if enemy.player_ref:
		animation.play("walk")
	else:
		play_walk_bilboard_camera(velocity)
		#play_walk_axis_bilboard(velocity.z, velocity.x)
	pass

func action_behavior():
	if enemy.can_hit:
		animation.play("damage")
		enemy.is_hitting = true
		enemy.is_attacking = false
	elif enemy.can_die:
		enemy.kill_enemy()
		pass
	elif enemy.can_attack and !enemy.is_attacking and !enemy.is_hitting:
		animation.play("attack")
		pass
	pass
	
func _on_animation_finished(anim_name:String)->void:
	match anim_name:
		"walk":
			idle_dir = "idle"
		"walk_up":
			idle_dir = "idle_up"
		"walk_right":
			idle_dir = "idle_right"
		"walk_left":
			idle_dir = "idle_left"
		"attack":
			enemy.can_attack = false
			animation.play(idle_dir)
		"damage":
			enemy.can_hit = false
			enemy.is_hitting = false
	pass

Code from AI movement(It’s a FSM and this is the idle state):

extends State
class_name Idle_State

var move_dir: Vector3
var wonder_time: float = 0
var time_moving: float = 0

func random_wonder():
	move_dir = Vector3(randf_range(-1, 1), 0, randf_range(-1, 1)).normalized()
	wonder_time = randf_range(1, 3)
	time_moving = randf_range(1, 2)

func Enter():
	random_wonder()
	
func _state_process(delta: float):
	if wonder_time > 0:
		wonder_time -= delta
	else:
		random_wonder()

func _state_physics_process(delta: float):
	if enemy and time_moving > 0:
		enemy.velocity = move_dir * enemy.speed
		time_moving -= delta
	elif enemy:
		enemy.velocity = Vector3(0,enemy.velocity.y,0)
	pass

Oh I also for got that forward is also -z

I think you got your player set correctly, but the enemy forward takes the positive z of the basis. Either way they should be the same signed direction

Comments inline

1 Like

Still not works, it looks crazier than before, i guess the problem could be the velocity of enemy, because i guess this function play_walk_bilboard_camera is considering the enemy is walking in one direction of the same axis not any direction or any axis, i don’t know how to solve this.

add this to your random walk direction

gloabal_transform.basis = Basis(Vector3.UP,  move_dir.angle_to(Vector3.FORWARD))
1 Like

this is from the video
image

I think you got off track here.

image

where he would flip the face of the animation

he also mentions that the camera is facing in the -z, and he decides that the enemy forward is +z. I think this is a mistake since all of godot 3d is -z is considered forward.

when making this change you will also need to adjust which rows are being set.

Not works properly too, i’ve changed the signal and add that line you mentioned to the random_wonder function:

Enemy AI Movement:

extends State
class_name Idle_State_Skeleton

var move_dir: Vector3
var wonder_time: float = 0
var time_moving: float = 0
var basis_enemy: Basis

func random_wonder():
	move_dir = Vector3(randf_range(-1, 1), 0, randf_range(-1, 1)).normalized()
	wonder_time = randf_range(1, 3)
	time_moving = randf_range(1, 2)
	enemy.global_transform.basis = Basis(Vector3.UP, move_dir.angle_to(Vector3.FORWARD))

func Enter():
	random_wonder()
	
func _state_process(delta: float):
	if wonder_time > 0:
		wonder_time -= delta
	else:
		random_wonder()

func _state_physics_process(delta: float):
	if enemy and time_moving > 0:
		enemy.velocity = move_dir * enemy.speed
		time_moving -= delta
	elif enemy:
		enemy.velocity = Vector3(0,enemy.velocity.y,0)
	pass

Sprite3D:

extends EnemyTexture
class_name Skeleton_Texture

@onready var idle_dir: String = "idle"
@export var anim_col: int = 0

func animate(velocity: Vector3):
	if enemy.can_hit or enemy.can_attack or enemy.can_die:
		action_behavior()
	else:
		move_behavior(velocity)

func move_behavior(velocity: Vector3):
	if velocity.x != 0 and velocity.z != 0 and !(enemy.is_attacking or enemy.is_retreating):
		play_walk(velocity)
	else:
		animation.play(idle_dir)
	pass

func play_walk_bilboard_camera(_velocity: Vector3):
	var player: Player_Skeleton = get_tree().get_first_node_in_group("Player")
	var p_fwd = -player.camera_principal.global_transform.basis.z
	var fwd = -enemy.global_transform.basis.z
	var left = -enemy.global_transform.basis.x
	#print("valor pwd:", p_fwd, " fwd:", fwd, " left:", left)
	#print("global basis:", enemy.global_transform.basis)
	
	var l_dot: float = left.dot(p_fwd)
	var f_dot: float = fwd.dot(p_fwd)
	var row: int = 0
	print("fdot: ",f_dot, " l_dot:", l_dot)
	#print("position diff: ", player.global_position.length() - enemy.global_position.length())
	if f_dot < -0.85:
		row = 0 #front sprite
	elif f_dot > 0.85:
		row = 3 #back sprite
	else:
		if l_dot > 0:
			row = 2 #right sprite
		elif abs(f_dot) < 0.3:
			row = 1 #left sprite
		elif f_dot < 0 :
			row = 1 #forward left sprite
		else:
			row = 0 #back sprite
	
	frame = anim_col + row * 9
	animation.play("walk_anim_col")
	
func play_walk(velocity: Vector3):
	#print("velocity x:", abs(velocity.x), "velocity z:", abs(velocity.z))
	if enemy.player_ref:
		animation.play("walk")
	else:
		play_walk_bilboard_camera(velocity)
		#play_walk_axis_bilboard(velocity.z, velocity.x)
	pass

func action_behavior():
	if enemy.can_hit:
		animation.play("damage")
		enemy.is_hitting = true
		enemy.is_attacking = false
	elif enemy.can_die:
		enemy.kill_enemy()
		pass
	elif enemy.can_attack and !enemy.is_attacking and !enemy.is_hitting:
		animation.play("attack")
		pass
	pass
	
func _on_animation_finished(anim_name:String)->void:
	match anim_name:
		"walk":
			idle_dir = "idle"
		"walk_up":
			idle_dir = "idle_up"
		"walk_right":
			idle_dir = "idle_right"
		"walk_left":
			idle_dir = "idle_left"
		"attack":
			enemy.can_attack = false
			animation.play(idle_dir)
		"damage":
			enemy.can_hit = false
			enemy.is_hitting = false
	pass

I’ve modified the movement just to see if his is walking and playing the animation of the right direction:

move_dir = Vector3(randf_range(-1, 1), 0, 0).normalized()

and

func move_behavior(velocity: Vector3):
	if velocity.x != 0 and velocity.z != 0 and !(enemy.is_attacking or enemy.is_retreating):
		play_walk(velocity)
	else:
		animation.play(idle_dir)
	pass

to

func move_behavior(velocity: Vector3):
	if (velocity.x != 0 or velocity.z != 0) and !(enemy.is_attacking or enemy.is_retreating):
		play_walk(velocity)
	else:
		animation.play(idle_dir)
	pass

He is going to move only up and down, but the animation keeps playing in the opposite direction of the enemy’s movement. Sometimes it plays correctly, but other times it behaves unpredictably, seemingly playing the animation randomly:

After struggling for a long time trying to figure out how to fix it, I finally did it. Thanks, @pennyloafers, for your support.

The problem was as I mentioned before, when I had the enemy move only along the z-axis, the code worked perfectly. However, when it moved along the x-axis, it started to bug out completely. The issue was that the code wasn’t considering the enemy’s velocity. Taking the velocity into account to determine which animation should play when it moves along the x-axis was the trick:

func row_by_axis(axis_lr: float, axis_ud: float)->int:
	if abs(axis_lr) > abs(axis_ud):
		if axis_lr < 0:
			return WALK.LEFT
		else:
			return WALK.RIGHT
	else:
		if axis_ud < 0:
			return WALK.DOWN
		else:
			return WALK.UP
	return 0

func play_walk_bilboard_camera(velocity: Vector3):
	var player: Player_Skeleton = get_tree().get_first_node_in_group("Player")
	var p_fwd = -player.camera_principal.global_transform.basis.z
	var fwd = -enemy.global_transform.basis.z
	var left = -enemy.global_transform.basis.x
	
	var l_dot: float = left.dot(p_fwd)
	var f_dot: float = fwd.dot(p_fwd)
	var row: int = 0
	if f_dot < -0.85:
		if velocity.x != 0:
			row = row_by_axis(velocity.z,velocity.x)
		else:
			row = WALK.DOWN #front sprite
	elif f_dot > 0.85:
		if velocity.x != 0:
			row = row_by_axis(-velocity.z, -velocity.x)
		else:
			row = WALK.UP #back sprite
	else:
		if l_dot > 0:
			if velocity.x != 0:
				row = row_by_axis(-velocity.x, -velocity.z)
			else:
				row = WALK.RIGHT #right sprite
		elif abs(f_dot) < 0.3:
			if velocity.x != 0:
				row = row_by_axis(velocity.x, velocity.z)
			else:
				row = WALK.LEFT #left sprite
		elif f_dot < 0 :
			if velocity.x != 0:
				row = row_by_axis(velocity.x, velocity.z)
			else:
				row = WALK.LEFT #forward left sprite
		else:
			row = WALK.DOWN
	frame = anim_col + row * 9
	animation.play("walk_anim_col")

I changed the numbers of rows to a enum to make it more readable and make my enemy just move to one axis and not both at the same time, it works like a charm.