Top Down, 2D, Tank Controls, animations for treads through animation_player not all playing

Godot Version

4.22

Question

Hello.
I am currently trying to program tank animations.

I have the tank treads as a separate sprite from the tank itself.

Each tread is the same sprite, flipped across the y axis.

The forward and backwards animations work as intended but the additional animations do not.

There is an animation for clockwise, counterclockwise, left u-turn, and right u-turn movement.

Clockwise and counterclockwise are for when the player presses only the left or right movement inputs to rotate the tank.

Uturns are for when the player moves forwards or backwards while rotating.

The animations play in the editor, but not while debugging.

Notably, while tapping the left and right inputs while moving forwards or backwards, it will move forward a single frame of the animation, only resetting when I stop moving forwards or backwards.

Here is the code I am working with right now:

extends CharacterBody2D

const SPEED = 64.0
const TURN_SPEED = 1
const ROTATION_SPEED = 20

@export var weapon: Node2D
@onready var body_sprite := $tank_body_sprite
@onready var animation_player = $AnimationPlayer
@onready var collision_shape := $tank_collision_shape
var direction: Vector2 = Vector2.RIGHT

func _physics_process(delta):
	var input_direction := Input.get_vector("cclockwise","clockwise","backward","forward")
	if input_direction.x != 0:
		#rotate direction based on input vector and apply turn speed, animate.
		direction = direction.rotated(input_direction.x * (PI / 2) * TURN_SPEED * delta)
		rotation = direction.angle()
		if input_direction.x >=0:
			animation_player.play('cclockwises')
		if input_direction.x <=0:
			animation_player.play('clockwises')
		
	if input_direction.y != 0:
		#forewards and backwards movement and animations.
		if input_direction.y <=0:
			animation_player.play('forwards')
		if input_direction.y >=0:
			animation_player.play('backwards')
		velocity = lerp(velocity, (direction.normalized() * input_direction.y) * SPEED, SPEED * delta)
		
		if input_direction.x != 0 and input_direction.y != 0:
			#rotating and moving simultaneously, play animations
			if (input_direction.y <=0 and input_direction.x <=0) or (input_direction.y >=0 and input_direction.x >=0):
				animation_player.play('uturnright')
			if (input_direction.y >=0 and input_direction.x <=0) or (input_direction.y <=0 and input_direction.x >=0):
				animation_player.play('uturnleft')
				
	else:
		animation_player.play('idles')
		velocity = Vector2.ZERO
	
	#apply movement and velocity
	move_and_slide()

I believe it has something to do with input_direction.x and how it is being used for rotation instead of velocity, but if I understand correctly, Input.get_vector should be a Boolean that detects if the respective input has been pressed and assigning a 1/-1 or 0 respectively to their assigned axis.

It should still be able to detect whether input_direction.x or .y <=0 or >=0 and play the animations like it does with the forwards and backwards animations.

I have tried doing Input.is_action_pressed(“”) if/ands to get this desired result, but it still has the same issue. Only forwards and backwards animations play.

Thank you in advance for your time.

var input_direction := Input.get_vector("cclockwise","clockwise","backward","forward")

Just to be sure. Do you have the animation name cclockwises correct? I see you are using it with 2 different spellings.

Yes. The s at the end of these words is not a typo. That is just a mental note that forward is an input, forwards is the animation. It sounds weird, but it’s just maintaining the pattern. clockwise is the input, clockwises is the animation. Good catch, but it is intended. “cclockwise” is also short for counter clockwise.

Would it be helpful to provide screenshots of my scene for this? Im at work, currently, but I would be able to provide visuals when I get home.

If you only hold down your turn keys left/right the animations will be cancelled by the idle animation in the else below because y is 0. Is that what you want?

No, but the Idle animation should only play when no inputs are being received. It should still be receiving the input_direction.x, either as a 1 or -1.
The input_direction.y animations play with their respective movements and moves as intended, it’s the input_direction.x animations that are not playing while the tank object moves as intended.

Vector2 get_vector ( StringName negative_x, StringName positive_x, StringName negative_y, StringName positive_y, float deadzone=-1.0 ) const

Every animation is the same speed, has 3 frames, and there’s no errors. The clockwises and cclockwises animations work identically to the forwards and backwards animations, they shouldn’t be being overridden by the idles animation until I stop pressing any of the listed inputs.

It’s either something really stupid, or something with the animation player I’m missing. I’m downloading OBS to provide visuals for this, stand by.

Edit: New users cannot upload attachments, so I’m going to upload it an link the video here: tankmovement - Sendvid

If you cant figure it out, you may want to upload part of the project to github or some other website. Maybe easier to get help.

I might hit up Stack Exchange, but they aren’t the most elaborative over there.
I am going to keep this version of the script separate for now.
I’ve spent a lot of time experimenting to see what might be confusing it, but it really just will not see the animation as things are.
I’m going to try an entirely different system of movement and animation, but I’d like to eventually figure out what’s going wrong here as a learning experience.
I’ll leave this open if anyone has ideas.

What happens if you replace the forward and backward animations in the code with cclockwises and clockwises? Just to see if they do play, but it’s a weird visual effect that when turning you just don’t see the frames change.

Also, the layout of the if statements could possibly do with an overhaul, if you replace the animation.play with print I pressed up and right and the output shows a list of

clockwises
forwards
uturnright 

So it looks like there’s a massive clash of what animation it was trying to play.

2 Likes

So it’s hideous, but I’ve gone and spelled out the button inputs for each animation as redundantly as I can for Godot for testing…

func _physics_process(delta):
	var input_direction := Input.get_vector("cclockwise","clockwise","backward","forward")
		#forwards animation check animation_player.play
	if Input.is_action_pressed("forward") and not (Input.is_action_pressed("clockwise") or Input.is_action_pressed("cclockwise")):
		animation_player.play('forwards')
		#backwards animation check
	if Input.is_action_pressed("backward") and not (Input.is_action_pressed("clockwise") or Input.is_action_pressed("cclockwise")):
		animation_player.play('backwards')
		#cclockwises animation check
	if Input.is_action_pressed("cclockwise") and not (Input.is_action_pressed("backward") or Input.is_action_pressed("forward")): 
		animation_player.play('cclockwises')
		#clockwises animation check
	if Input.is_action_pressed("clockwise") and not (Input.is_action_pressed("backward") or Input.is_action_pressed("forward")):
		animation_player.play('clockwises')
		#forward right uturn animation check
	if Input.is_action_pressed("clockwise") and Input.is_action_pressed("forward"):
		animation_player.play('uturnright')
		#forward left uturn animation check
	if Input.is_action_pressed("cclockwise") and Input.is_action_pressed("forward"):
		animation_player.play('uturnleft')
		#backward left uturn animation check
	if Input.is_action_pressed("clockwise") and Input.is_action_pressed("backward"):
		animation_player.play_backwards('uturnleft')
		#backward right uturn animation check
	if Input.is_action_pressed("cclockwise") and Input.is_action_pressed("backward"):
		animation_player.play_backwards('uturnright')

It’s if-and-not clown vomit, but using that print trick, I’ve found that the idle animation was also and still is conflicting with the clockwises and cclockwises animations, which re-contextualizes the issue a little.
Godot knows I am pressing the left and right keys and only those keys to activate this animation and the corresponding movement, which should invalidate the else statement until I stop pressing them, but for some reason the else statement is triggering during these button presses regardless.

Noticeably, when I have the run all other animations- and two new animation_player.play_backwards() variations of the uturns, it… mostly works? Animation Player doesn’t seem to like animating what I’m asking it to in reverse. At least, not while the button is pressed. Using animation_player.stop() and .pause() still has trouble playing the backwards uturns and the rotating animations. Compared to earlier, it’s progress. I could try and find out why .play_backwards is having trouble, or I could just circumvent that with a separate animation playing forwards.

I’ve thought of an entirely new control scheme that might actually be more fun and simpler to build, but I’d like to learn what rookie things I’m doing wrong here before I do it again somewhere worse.

Alright, so:

I still haven’t been able to correct the original code.
I have no idea why the “else” statement was conflicting when the “if” statements were being conditionally met.
That said:
I have completely reworked the movement code and the animation code at the same time.

Exhibit 1:

How it works is there are two animation players, and each animation player is tied to a single forward tread animation.

I’ve tied each tread to a linear speed and two separate speeds that are additively calculated to make the final speed scale, and made an equal number of maximum speeds to interpolate towards and not exceed.
Negative speed scales will play in reverse.
With some more work, I can make them and the actual velocity of the tank rely on acceleration, but this is ultimately a better solution than I originally intended, and wanted to provide what I’ve learned since posting this.

Here is the code as it is now:

extends CharacterBody2D

@export var weapon: Node2D
@onready var body_sprite := $tank_body_sprite
@onready var collision_shape := $tank_collision_shape
@onready var left_tread_anim= $tank_tread_right_anim_player
@onready var right_tread_anim = $tank_tread_left_anim_player
var direction: Vector2 = Vector2.RIGHT
var speed = 60
#actual speeds
var turn_speed = 1.0
var rotation_speed = 20.0
var acceleration = 2.0
var maxspeed = 60.0
#tread animation speeds, for calculating each tread's speed_scale separately.
var tread_linearspeed = 0.0
var right_tread_speed = 0.0
var left_tread_speed = 0.0
var max_tread_turnspeed = 4.0
var max_tread_linearspeed = 6.0
#miscellaneous variables
var tread_speed_lerpweight = 0.75

func _physics_process(delta):
	#play tread animations, set their speed scales, get inputs.
	right_tread_anim.play('right_tread_anim')
	left_tread_anim.play('left_tread_anim')
	right_tread_anim.set_speed_scale(tread_linearspeed + right_tread_speed)
	left_tread_anim.set_speed_scale(tread_linearspeed + left_tread_speed)
	var input_direction := Input.get_vector("cclockwise","clockwise","backward","forward")
		#forwards animation check animation_player.play
	if input_direction.x != 0:
		#rotate direction based on input vector and apply turn speed
		#rotation has two identical "rotation =" lines. 
		#The first allows the tank to "brake and sway" into it's uturn rotation and animation, making a heftier motion.
		#The last one double checks the current rotation and interpolates the tank's actual rotation, realigning the sprite.
		#Without this last line under the else statement, the tank drifts at an odd angle and does not correct itself.
		#Without the first line, the inertia effect is absent and entirely different, rotating around an unseen circle while the sprite remains completely still.
		direction = direction.rotated(input_direction.x * (PI / 2.0) * turn_speed * delta)
		rotation = lerp_angle(rotation, direction.angle(), 0.1)
		if input_direction.x >= 0:
			left_tread_speed = lerp((left_tread_speed + input_direction.x), max_tread_turnspeed, tread_speed_lerpweight)
			right_tread_speed = lerp((right_tread_speed - input_direction.x),(max_tread_turnspeed * -1), tread_speed_lerpweight)
		if input_direction.x <= 0:
			right_tread_speed = lerp((right_tread_speed + input_direction.x), max_tread_turnspeed, tread_speed_lerpweight) 
			left_tread_speed = lerp((left_tread_speed - input_direction.x), (max_tread_turnspeed * -1), tread_speed_lerpweight)
	else:
		#reduce tread animation speed, correct rotation. 
		left_tread_speed = lerp(left_tread_speed, 0.0, tread_speed_lerpweight)
		right_tread_speed = lerp(right_tread_speed, 0.0, tread_speed_lerpweight)
		rotation = lerp_angle(rotation, direction.angle(), 0.1)	
		
	if input_direction.y != 0:
		#forewards and backwards movement
		velocity = lerp(velocity, (direction * input_direction.y) * speed, speed * delta)
		tread_linearspeed = lerp(tread_linearspeed + input_direction.y, max_tread_linearspeed * input_direction.y, tread_speed_lerpweight)
		
	else:
		velocity = lerp(velocity, Vector2.ZERO, 0.1)
		tread_linearspeed = lerp(tread_linearspeed, 0.0, tread_speed_lerpweight)

	#apply movement and velocity
	move_and_slide()
	
func _input(event):
	pass

Now, for some special mentions regarding the two “rotation =” lines under the direction.x “if” statement:
They are responsible for the inertia of the tank’s rotation, especially while moving forwards or backwards. There’s a noticeable shift in weight, which paired with the animation system, makes for an incredibly weighty movement. I discovered this on accident, but it had some issues at first, which leads me to these strange behaviors.

If the LAST line is removed, under the “else” statement, the tank rotates, but the sprite only mostly rotates, resulting in a weird “drifting” motion. The object is physically pointed where it is moving towards, but the sprite’s rotation is askew. The object rotates, and the tank’s sprites have a sort of deadzone where, once exceeded, the sprite will rotate, but without the confirmation in the “else” statement, will not straighten out on their own.

While removing the FIRST line under the initial “if” statement does… this.

This last one has interesting implications, and would make for an interesting enemy flight pattern for, say, airborne VTOL drones. I’ll try and reverse engineer this one into something useable.

Thank you all for your help on this, once again. I won’t be updating this thread further myself.

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