How to flip a billboard player character?

Godot Version

4.4

Question

So I’ve been working on a small scale basic prototype for a larger project I want to dive into in the future and I’ve run into a roadblock. The world is 3D, the characters are 2D hand drawn animations attached to an AnimatedSprite3D and the way I was able to get the basics working was by billboarding them. But I can’t seem to find a good solution to get the character to face left and right based on player input. Any help would be appreciated.

IIRC you can mark individual animation cels as flipped in the animation editor; if you don’t have too much animation you could just duplicate and flip so you have run_left and run_right and so forth.

So the character is going to have several attack animations as well as general movement, damage sprites, and other interactive elements. For the number of animations/situations, I’m not sure duplicating would be a viable solution?

Have you tried setting flip_h = true on the sprite? I’m not sure if the billboarding will interfere with that. At the worst, you could use a custom shader that just optionally reverses the U coord.

I tried that at one point and it did make the character flip but not based on the player input, I think it was flipping based on global coordinates, so he would flip when you rotated the camera.

I’d try the shader, then.

How do you mean?

Can you share your code for flipping? You can assing the value based on player input

flip_h = input_dir.x < 0

extends CharacterBody3D

@export_group(“Camera”)
@export_range(0.0, 1.0) var mouse_sensitivity := 0.25

@export_group(“Movement”)
@export var move_speed := 8.0
@export var acceleration := 20.0
@export var rotation_speed := 12.0
@export var jump_impulse := 12.0

var is_attacking: bool = false
var _camera_input_direction := Vector2.ZERO
var _last_movement_direction := Vector3.BACK
var _gravity := -30.0

@onready var _camera_pivot: Node3D = %CameraPivot
@onready var _camera: Camera3D = %Camera3D
@onready var animated_sprite_3D: AnimatedSprite3D = $AnimatedSprite3D
@onready var animation_tree: AnimationTree = $AnimationTree

Initiate and Exit

func _input(event: InputEvent) → void:
if event.is_action_pressed(“left_click”):
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if event.is_action_pressed(“ui_cancel”):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE

Camera

func _unhandled_input(event: InputEvent) → void:
var is_camera_motion := (
event is InputEventMouseMotion and
Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED
)
if is_camera_motion:
_camera_input_direction = event.screen_relative * mouse_sensitivity

func _physics_process(delta: float) → void:
_camera_pivot.rotation.x += _camera_input_direction.y * delta
_camera_pivot.rotation.x = clamp(_camera_pivot.rotation.x, -PI / 6.0, PI / 3.0)
_camera_pivot.rotation.y -= _camera_input_direction.x * delta

_camera_input_direction = Vector2.ZERO

var raw_input := Input.get_vector("move_left", "move_right", "move_up", "move_down")
var forward := _camera.global_basis.z
var right := _camera.global_basis.x

var move_direction := forward * raw_input.y + right * raw_input.x
move_direction.y = 0.0
move_direction = move_direction.normalized()

var y_velocity := velocity.y
velocity.y = 0.0
velocity = velocity.move_toward(move_direction * move_speed, acceleration * delta)
velocity.y = y_velocity + _gravity * delta

var is_starting_jump := Input.is_action_just_pressed("jump") and is_on_floor()
if is_starting_jump:
	velocity.y += jump_impulse

move_and_slide()
handle_animation()

Movement

if move_direction.length() > 0.2:
	_last_movement_direction = move_direction
var _target_angle := Vector3.BACK.signed_angle_to(_last_movement_direction, Vector3.UP)

func handle_animation():

if is_on_floor():  
	if velocity:
		animated_sprite_3D.play("Marco_Run") 
		is_attacking == false
	else:
		animated_sprite_3D.play("Marco_Idle")
		is_attacking == false
	
Timer

if Input.is_action_just_released("jump"):
	animated_sprite_3D.play("Marco_Jump")
	if velocity.y > 0:
		animated_sprite_3D.play("Marco_Rising")
elif not is_on_floor() and velocity.y < 0:
	animated_sprite_3D.play("Marco_Falling")

Combat

if Input.is_action_just_pressed("punch"):
	animated_sprite_3D.play("Marco_Punch");
	is_attacking = true

I don’t see flip_h in your code

I removed what code I had for that a few days ago, I don’t remember exactly how it was written. I just remember it involved global coordinates. That’s my whole character script.

Ok, then I’d say give animated_sprite_3D.flip_h = raw_input.x < 0 a try

1 Like

It seems like he’s flipping at random but the sprite is flipping now, the direction he’s facing doesn’t match up with the specific input. Specifically he faces left when pushing left, sometimes he faces right when pushing right, most the time he’s facing left while pushing right.

Sorry, forgot the < 0 in my last post.

1 Like

HEEEY that worked :smiley: now I just need a line to make it remember to face the direction of the last input. I added this

animated_sprite_3D.flip_h = raw_input.x < 0

if _last_movement_direction.x < 0:
	$AnimatedSprite3D.flip_h = true
elif _last_movement_direction.x > 0:
	$AnimatedSprite3D.flip_h = false

that didn’t seem to do the trick :sweat_smile:

Your move direction is altered by the camera’s rotation, but the raw_input works because it is not.

Maybe you could avoid setting the flip_h when your raw_input is not zero

# copying the 0.2 value from your "last_move_direction"
if absf(raw_input.x) > 0.2:
    animated_sprite_3D.flip_h = raw_input.x < 0

Try tweaking the 0.2 lower if it doesn’t work well, though your movement is .normalized() so it doesn’t support in-between lengths like 0.1 or even 0.7 if you intend to add controller support.

3 Likes

AYYYYY that did it, didn’t even have to tweak it from what you posted. Thank you so much for the help, that was driving me crazy hahaha. I also really appreciate the explanation, that helps a lot.

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