How to fix my weapon attack movement based on mouse position?

Godot Version

4.2.1

Question

I currently have my weapon rotating around my player based on the mouse position and if the mouse is to the right I would like the attack to move clockwise and if the mouse position the attack should proceed counter-clockwise.

I attempted to do this by flip_h and scale -1 the weapon sprite, but it just rotated the sprite itself, it didn’t change the attack animation, it still rotates the same but the sprite is just flipped. I also attempted to flip_h and scale -1 the actual weapon_pivot node too with no luck.

Please let me know if additional information is needed.

player.gd:

extends CharacterBody2D

class_name Player

signal healthChanged

@export var speed: int = 100
@export var knockbackPower: int = 1000
@export var maxHealth: int = 3

@onready var animations: AnimationPlayer = $playerAnimations
@onready var effects: AnimationPlayer = $Effects
@onready var arrowAnimations: AnimationPlayer = $arrowAnimations

@onready var hurtTimer: Timer = $hurtTimer
@onready var currentHealth: int = maxHealth
@onready var bow: Area2D = $bow_pivot/bow
@onready var bow_pivot: Node2D = $bow_pivot
@onready var arrow = preload("res://actors/player/weapons/arrow.tscn")
@onready var arrowNode2D: Node2D = $arrowNode2D
@onready var weaponMaster: Node2D = $weaponMaster
@onready var weapon_pivot: Node2D = $weaponMaster/weapon_pivot
@onready var weapon: Node2D = $weaponMaster/weapon_pivot/weapon
@onready var weaponAnimation: AnimationPlayer = $weaponMaster/weaponAnimationPlayer

var lastAnimDirection: String = "Right"
var isHurt: bool = false
var isArrowCharging: bool = false
var isArrowCharged: bool = false
var isArrowFired: bool = false
var enemyCollisions = []
var bow_equipped: bool = false
var bow_cooldown: bool = true
var mouseLocFromPlayer = null
var bowCharge = 0.0
var maxBowCharge = 1.0
var bowChargeSpeed = 1.0

func _ready():
	effects.play("RESET")
	arrowAnimations.play("RESET")
	weaponAnimation.play("RESET")
	bow.visible = false

func handleInput():
	var moveDirection = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") 
	velocity = moveDirection * speed 
	if Input.is_action_just_pressed("attack"):
		attack()

func attack():
	if bow_equipped: return
	mouseLocFromPlayer = get_global_mouse_position() - self.position
	var mouse_pos = get_global_mouse_position()
	$weaponMarker2D.look_at(mouse_pos)
	var direction = "Right"
	if mouseLocFromPlayer.x > 0:
		direction = "Right"
	elif mouseLocFromPlayer.x < 0:
		direction = "Left"
	if direction == "Left":
		weaponMaster.scale.x = -1
		weaponAnimation.play("meleeAttack")
		await weaponAnimation.animation_finished
		weaponMaster.scale.x = 1
	elif direction == "Right":
		weaponMaster.scale.x = 1
		weaponAnimation.play("meleeAttack")

func updateAnimation():
	mouseLocFromPlayer = get_global_mouse_position() - self.position
	var mouse_pos = get_global_mouse_position()
	$weaponMarker2D.look_at(mouse_pos)
	var direction = "Right"
	if mouseLocFromPlayer.x > -9:
		# Mouse is to the right of the player
		direction = "Right"
	elif mouseLocFromPlayer.x < -9:
		# Mouse is to the left of the player
		direction = "Left"
	if velocity.length() == 0:
		animations.play("idle" + direction)
	else:
		animations.play("walk" + direction)
	lastAnimDirection = direction


func knockback(enemyPosition: Vector2):
	var knockbackDirection = (enemyPosition - position).normalized() * knockbackPower
	velocity = knockbackDirection
	move_and_slide()

func handleCollision():
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		var collider = collision.get_collider()
		print_debug(collider.name)

func player():
	pass

func _physics_process(delta):
	handleInput()
	move_and_slide()
	updateAnimation()
	handleCollision()
	bowAttack(delta)
	if !isHurt:
		for enemyArea in enemyCollisions:
			hurtByEnemy(enemyArea)
	
	mouseLocFromPlayer = get_global_mouse_position() - self.position
	var mouse_pos = get_global_mouse_position()
	$bowMarker2D.look_at(mouse_pos)
	$weaponMarker2D.look_at(mouse_pos)
	$Marker2D.look_at(mouse_pos)
	
	if !bow_equipped:
		if mouseLocFromPlayer.x < -9:
			weapon_pivot.scale.x = -1
			weapon_pivot.rotation = $weaponMarker2D.rotation + 70
		elif mouseLocFromPlayer.x >= -9:
			weapon_pivot.scale.x = 1
			weapon_pivot.rotation = $weaponMarker2D.rotation + 90
	
	# Rotate the bow pivot based on the angle between the player and the mouse
	elif bow_equipped:
		bow_pivot.rotation = $bowMarker2D.rotation - 90
	
func bowAttack(delta):
	if Input.is_action_just_pressed("bowEquip"):
		if bow_equipped:
			weapon.enable()
			bow_equipped = false
			bow.visible = false
		else:
			weapon.disable()
			bow_equipped = true
			bow.visible = true
			
	if Input.is_action_pressed("bowAttack") and bow_equipped and bowCharge <= maxBowCharge:
		isArrowCharging = true
		bowCharge += delta * bowChargeSpeed
	else:
		if bowCharge < maxBowCharge:
			if Input.is_action_just_released("bowAttack"): return
		elif bowCharge >= maxBowCharge:
			isArrowCharging = false
			isArrowCharged = true
			if Input.is_action_just_released("bowAttack"):
				isArrowCharged = false
				isArrowFired = true
				bowFire()

	if isArrowCharging:
		# Customize behavior when the arrow is charged
		effects.play("chargeFlash")
	if isArrowCharged:
		# Customize behavior when the arrow is charged
		effects.play("correctTiming")
	if isArrowFired:
		# Customize behavior when the arrow is fired
		effects.play("RESET")

func bowFire():
	var arrow_instance = arrow.instantiate()
	arrow_instance.rotation = $Marker2D.rotation
	arrow_instance.global_position = $Marker2D.global_position
	add_child(arrow_instance)
	bowCharge = 0.0
	isArrowFired = false

func hurtByEnemy(area):
	currentHealth -= 1
	if currentHealth == 0:
		animations.play("death")
		await animations.animation_finished
		queue_free()
		# Activate the Game Over screen here
	else:
		healthChanged.emit(currentHealth)
		isHurt = true
		knockback(area.get_parent().velocity)

Additionally the attack doesn’t start based on the current position of the weapon’s rotation around the pivot, it is based on the animation degrees that were keyed in. I will provide a photo of my animation in a comment.

EDIT: I added an additional Node2D and the attack now rotates counter-clockwise, however the attack is not happening based on the mouse position.

Also the animation when executed with the mouse.x < 0 isn’t the full animation. It starts with the weapon horizontal with the player and rotates down below and behind the player. and the slime cannot be damaged.


*Note: There is an additional unnamed Marker2D in the tree, it is just cut off.

Here is a current video: https://youtu.be/nu2Rg6xpgnQ

Edit 2: I added a key frame to the attack animation that is weaponMaster.scale.x = -1, y = 1 and that played fine. I think it might be all related to how everything is looking at my mouse. However, I attempted a new player script that controls the scaling in the updateAnimation function using a state and it still also performed the same as the video above.

mouseLocFromPlayer = get_global_mouse_position() - self.position

This looks problematic the self position (player world position) may exceed mouse screen space position. You may want to fire a ray into the camera from the player to get player screen position. Only if the camera is fixed will this work.

1 Like

Can you please elaborate a little bit?

You may want to fire a ray into the camera from the player to get player screen position.

Hmm, you can cast rays for 3d cameras but 2d is different.

I see that the get_global_mouse_position should resolve this. But I think you should then use self.global_position rather then position. If you spawn your player at some location other then the origin it will be messed up.

1 Like

Ahh, good to know, I am not even there yet, right now I am just spawning at the same place every time because my world is currently just my player and a slime on a grey background as I work on combat. Changing it to global didn’t affect either of my issues, good or bad, so I will leave it and start using that.

I resolved this using tweens:

extends CharacterBody2D

class_name Player

signal healthChanged

@export var speed: int = 100
@export var knockbackPower: int = 1000
@export var maxHealth: int = 3

@onready var animations: AnimationPlayer = $playerAnimations
@onready var effects: AnimationPlayer = $Effects
@onready var arrowAnimations: AnimationPlayer = $arrowAnimations

@onready var hurtTimer: Timer = $hurtTimer
@onready var currentHealth: int = maxHealth
@onready var bow: Area2D = $bow_pivot/bow
@onready var bow_pivot: Node2D = $bow_pivot
@onready var arrow = preload("res://actors/player/weapons/arrow.tscn")
@onready var arrowNode2D: Node2D = $arrowNode2D
@onready var weaponMaster: Node2D = $weaponMaster
@onready var weapon_pivot: Node2D = $weaponMaster/weapon_pivot
@onready var weapon: Node2D = $weaponMaster/weapon_pivot/weapon
@onready var weaponAnimation: AnimationPlayer = $weaponMaster/weaponAnimationPlayer

var lastAnimDirection: String = "Right"
var isHurt: bool = false
var isArrowCharging: bool = false
var isArrowCharged: bool = false
var isArrowFired: bool = false
var enemyCollisions = []
var bow_equipped: bool = false
var bow_cooldown: bool = true
var isAttacking: bool = false
var mouseLocFromPlayer = null
var bowCharge = 0.0
var maxBowCharge = 1.0
var bowChargeSpeed = 1.0

func _ready():
	effects.play("RESET")
	arrowAnimations.play("RESET")
	bow.visible = false

func handleInput():
	var moveDirection = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") 
	velocity = moveDirection * speed 
	
func attack():
	var mouseLocFromPlayer = get_global_mouse_position() - self.global_position
	var mouse_pos = get_global_mouse_position()
	var direction = "Right" if mouseLocFromPlayer.x > -9 else "Left"
	var currentWeaponPivot_rotation = weapon_pivot.rotation_degrees
	var target_pivot_rotation = currentWeaponPivot_rotation + (180 if direction == "Right" else -180)

	$Marker2D.look_at(mouse_pos)

	if Input.is_action_just_pressed("attack") and !isAttacking:
		if direction == "Right":
			if bow_equipped: return
			isAttacking = true
			weaponTween(currentWeaponPivot_rotation, target_pivot_rotation)
			isAttacking = false
		elif direction == "Left":
			weaponMaster.scale.x = -1
			if bow_equipped: return
			isAttacking = true
			weaponTween(currentWeaponPivot_rotation, target_pivot_rotation)
			isAttacking = false
			weaponMaster.scale.x = 1

func weaponTween(start_rotation, target_rotation):
	var tween = get_tree().create_tween()
	tween.tween_property(weapon_pivot, "rotation_degrees", start_rotation, 0.0)
	tween.tween_property(weapon_pivot, "rotation_degrees", target_rotation, 0.2)
	tween.tween_property(weapon_pivot, "rotation_degrees", start_rotation, 0.2)
	
		
func updateAnimation():
	mouseLocFromPlayer = get_global_mouse_position() - self.position
	var mouse_pos = get_global_mouse_position()
	$Marker2D.look_at(mouse_pos)
	var direction = "Right"
	if mouseLocFromPlayer.x > -9:
		# Mouse is to the right of the player
		direction = "Right"
	elif mouseLocFromPlayer.x < -9:
		# Mouse is to the left of the player
		direction = "Left"
	if velocity.length() == 0:
		animations.play("idle" + direction)
	else:
		animations.play("walk" + direction)
	lastAnimDirection = direction

func knockback(enemyPosition: Vector2):
	var knockbackDirection = (enemyPosition - position).normalized() * knockbackPower
	velocity = knockbackDirection
	move_and_slide()

func handleCollision():
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		var collider = collision.get_collider()
		print_debug(collider.name)

func player():
	pass

func _physics_process(delta):
	handleInput()
	move_and_slide()
	updateAnimation()
	handleCollision()
	attack()
	bowAttack(delta)
	if !isHurt:
		for enemyArea in enemyCollisions:
			hurtByEnemy(enemyArea)
	
	mouseLocFromPlayer = get_global_mouse_position() - self.position
	var mouse_pos = get_global_mouse_position()
	$Marker2D.look_at(mouse_pos)
	
	if !bow_equipped:
		if mouseLocFromPlayer.x < -9:
			weapon_pivot.scale.x = -1
			weapon_pivot.rotation = $Marker2D.rotation + 70
		elif mouseLocFromPlayer.x >= -9:
			weapon_pivot.scale.x = 1
			weapon_pivot.rotation = $Marker2D.rotation + 90

			
	
	# Rotate the bow pivot based on the angle between the player and the mouse
	elif bow_equipped:
		bow_pivot.rotation = $Marker2D.rotation - 90
	
func bowAttack(delta):
	if Input.is_action_just_pressed("bowEquip"):
		if bow_equipped:
			weapon.enable()
			bow_equipped = false
			bow.visible = false
		else:
			weapon.disable()
			bow_equipped = true
			bow.visible = true
			
	if Input.is_action_pressed("bowAttack") and bow_equipped and bowCharge <= maxBowCharge:
		isArrowCharging = true
		bowCharge += delta * bowChargeSpeed
	else:
		if bowCharge < maxBowCharge:
			if Input.is_action_just_released("bowAttack"): return
		elif bowCharge >= maxBowCharge:
			isArrowCharging = false
			isArrowCharged = true
			if Input.is_action_just_released("bowAttack"):
				isArrowCharged = false
				isArrowFired = true
				bowFire()

	if isArrowCharging:
		# Customize behavior when the arrow is charged
		effects.play("chargeFlash")
	if isArrowCharged:
		# Customize behavior when the arrow is charged
		effects.play("correctTiming")
	if isArrowFired:
		# Customize behavior when the arrow is fired
		effects.play("RESET")

func bowFire():
	var arrow_instance = arrow.instantiate()
	arrow_instance.rotation = $Marker2D.rotation
	arrow_instance.global_position = $Marker2D.global_position
	add_child(arrow_instance)
	bowCharge = 0.0
	isArrowFired = false

func hurtByEnemy(area):
	currentHealth -= 1
	if currentHealth == 0:
		animations.play("death")
		await animations.animation_finished
		queue_free()
		# Activate the Game Over screen here
	else:
		healthChanged.emit(currentHealth)
		isHurt = true
		knockback(area.get_parent().velocity)

Please note, that you are mixing different coordinate systems.
CanvasItem.get_global_mouse_position() is in the coordinate system of the canvas layer.
Node2D.position is in the coordinate system of the parent node.

While it works in certain cases, there are other cases (for example when the parent item is another Node2D), where this will not work.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.