CharacterBody2D auto move / bounce on its own when standing on a moving platform (AnimatableBody2D)

Godot Version

4.5.1

Question

Hi all, this is my first Godot project (mainly for training purpose).

Both my player and platform has no error when running (no script syntax errors) but when standing on a moving platform in idle state (no input detected), the player auto slide / bounce on the platform which lead to weird movement.

I have the sample video of this bug on Google drive below:

  • When the player stand on the platform, I stopped all the input, the player is in idle state (confirmed via console output) yet the player keep sliding left / right with the platform moving horizontally, or bouncing (switching between idle / fall stare) continuously with platform moving vertically

This bug has had me stalled for 2 weeks, somehow I tried to narrow down the cause (which I hope it’s correct).

For testing purpose, on the same moving platform nodes, I tried to clone my player → delete all state machine node → Apply this basic movement script then it WORKED (player standing still on moving platform, no weird sliding / bouncing). BUT when I inserted state machine and using this script as the stating state, the same bug happened.

extends CharacterBody2D


const SPEED = 100.0
const JUMP_VELOCITY = -400.0


func _physics_process(delta):
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction = Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

So I GUESS that the state machine maybe is the problem, but no matter what I fix the problem still exist…

Below are my related scripts (the one that currently using state machine, not the testing one):

  • Player (calling state machine)
extends CharacterBody2D
class_name Player

@onready var game_manager = %GameManager
@onready var state_machine_action = $State_machine/State_machine_action
@onready var animation_player_action = $Animation_player/AnimationPlayer_action
@onready var animation_player_status = $Animation_player/AnimationPlayer_status
@onready var audio_stream_player_2d = $AudioStreamPlayer2D
@onready var sprite = $Sprite

@onready var bullet_scene = preload("res://scenes/bullet.tscn")
@onready var bullet_shoot_position = $Sprite/Bullet_shoot_position

@export var power_multipler: float

# Assign the Player node itself to state machine
func _ready():
	state_machine_action.init(self, animation_player_action, audio_stream_player_2d)

func _process(delta):
	state_machine_action._process(delta)	
	
func _physics_process(delta):	
	state_machine_action._physics_process(delta)
	
func _shoot_bullet():
	var new_bullet = bullet_scene.instantiate()
	var shooting_direction = 1 #facing right by default
	
	shooting_direction = -1 if sprite.scale.x == -1 else 1
	
	# Shoot in the opposite direction when wall sliding
	if is_on_wall_only():
		shooting_direction = get_wall_normal().x
		
	new_bullet.direction = Vector2(shooting_direction, 0)
	new_bullet.position = bullet_shoot_position.global_position
	
	get_parent().add_child(new_bullet)
	new_bullet.get_node("AudioStreamPlayer2D").play()

func _air_slash():
	if state_machine_action.Current_state.name == "Jump" or state_machine_action.Current_state.name == "Fall":
		animation_player_action.play("slash_3")
	
# List of interacting item
func _on_interactive_scanner_found_interactable(target_node):
	if target_node is coin:
		game_manager._add_coin()

  • State machine (calling each state)
extends Node
class_name State_machine

@export var Starting_state: State
var Current_state: State

func init(controlled_player: CharacterBody2D, animation_player: AnimationPlayer, audio_player: AudioStreamPlayer2D):
	# Assign all the child node (all states) to the Player
	for child in get_children():
		if child is State:
			child.controlled_player = controlled_player	
			child.animation_player = animation_player
			child.audio_player = audio_player
	
	# Start the initial state
	_change_state(Starting_state)	
	
func _change_state(new_state: State):
	if Current_state != null:
		Current_state.exit()
	
	Current_state = new_state
	Current_state.enter()

func _process(delta):
	var new_state = Current_state._state_process(delta)
	if new_state != null:
		_change_state(new_state)
	
func _unhandled_input(event):
	var new_state = Current_state._state_unhandled_input(event)
	if new_state != null:
		_change_state(new_state)

func _physics_process(delta):	
	var new_state = Current_state._state_physics_process(delta)
	if new_state != null:
		_change_state(new_state)
  • base State
extends Node
class_name State

@export var animation_name: String
@export var sound_effect: String
@export var duration_timer: float

var controlled_player: CharacterBody2D
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
var animation_player
var audio_player

func enter():
	#Played the desired animation
	animation_player.play(animation_name)
	
	#Played the desired audio track
	_audio_handler()
	
	print("ENTERING STATE: ", self.name, " | animation: ", animation_name)

func _audio_handler():
	if sound_effect != "":
		var path = "res://assets/sounds/%s.wav" % sound_effect
		var audio_stream = load(path) as AudioStream
		
		if audio_stream != null:
			audio_player.stream = audio_stream
			audio_player.play()
		else:
			print("failed to load audio at: " + path)	

func exit():
	pass

func _state_physics_process(delta: float) -> State:
	return null
	
func _state_process(delta: float) -> State:
	return null		
	
func _state_unhandled_input(event: InputEvent) -> State:
	return null	

func _cooldown_ended(timer: float) -> bool:
	if timer <= 0:
		return true
	else:
		return false

  • Idle State (extend from State)
extends State

#IDLE STATE

#List of available state 
@export_category("Available states")
@export var Move_state: State
@export var Dash_state: State
@export var Rising_dash_state: State
@export var Jump_state: State
@export var Fall_state: State
@export var Slash_1_state: State

@onready var air_dash = $"../Air_dash"

func enter():
	super()
	controlled_player.velocity.x = 0
	air_dash.current_air_dash_amount = air_dash.max_air_dash_amount
	
func _state_physics_process(delta) -> State:		
	if not controlled_player.is_on_floor():
		return Fall_state
	
	controlled_player.move_and_slide()	 
				
	return null	

	
func _state_unhandled_input(event: InputEvent) -> State:
	if Input	.is_action_just_pressed("shoot"):
			controlled_player._shoot_bullet()		
	if controlled_player.is_on_floor():
		if Input.is_action_pressed("move_left") or Input.is_action_pressed("move_right"):
			return Move_state
		if Input.is_action_pressed("up") and Input.is_action_pressed("melee"):
			return Rising_dash_state
		if Input.is_action_just_pressed("jump"):
			return Jump_state
		if Input.is_action_just_pressed("dash"):
			return Dash_state	 
		if Input	.is_action_just_pressed("melee"):
			return Slash_1_state

	return null

Even adding platform_velocity to player velocity won’t fixed it so currently I’m stuck :face_with_head_bandage: . I hope that this is not a Godot bug but just my skill issue.

Thanks in advance.

Hey!

None of the script you shared have the player controller (the move state machine) script,can you also share that?

And I saw your post also on reddit ,it seems like on the horizontal moving platform the player is crouching for some reason.

Hi, the “crouching” that you mentioned actually is the animation of the “fall“ state (when not on floor and has gravity applied to make the player falls). My player switches to fall state when it slides to the edge of the platform (about to fall)

Below is the script for my player’s move state and fall state

  • Move state
extends State

#MOVE STATE

#List of available state 
@export_category("Available states")
@export var Idle_state: State
@export var Dash_state: State
@export var Jump_state: State
@export var Wall_jump_state: State
@export var Fall_state: State
@export var Slash_1_state: State

@export_category("Input var")
@export var move_speed = 200

func enter():
	super()
	controlled_player.velocity.y = 0

func _state_physics_process(delta) -> State:
	
	var direction = Input.get_axis("move_left", "move_right")
	controlled_player.velocity.x = direction * move_speed	
	
	if direction != 0:
		controlled_player.velocity.x = direction * move_speed
		
		if direction < 0:
			controlled_player.sprite.scale.x = -1
		else:
			controlled_player.sprite.scale.x = 1		
						
	elif direction == 0:
		controlled_player.velocity.x = move_toward(controlled_player.velocity.x, 0, move_speed)	
		return Idle_state
	
	if not controlled_player.is_on_floor():
		return Fall_state	

	controlled_player.move_and_slide()
		
	return null	
	
	
func _state_unhandled_input(event: InputEvent) -> State:
	if Input	.is_action_just_pressed("shoot"):
		controlled_player._shoot_bullet()
	
	if controlled_player.is_on_floor():		
		if Input.is_action_just_pressed("jump") :
			if not controlled_player.is_on_wall():
				return Jump_state
			else:
				return Wall_jump_state	#wall jump when player is colliding will wall
		if Input.is_action_just_pressed("dash"):
			return Dash_state	 
		if Input.is_action_just_pressed("melee"):
			return Slash_1_state				

	return null

  • Fall state
extends State

#FALL STATE

@onready var jump = $"../Jump"
@onready var air_dash = $"../Air_dash"

#List of available state 
@export var Idle_state: State
@export var Move_state: State
@export var Jump_state: State
@export var Air_dash_state: State
@export var Wall_slide_state: State
@export var Slam_down_state: State

#List of input var
@export var jump_time_to_fall: float
@export var move_speed: float
@export var jump_buffer: float

@onready var jump_height = jump.jump_height
@onready var fall_gravity = ((-2.0 * jump_height) / (jump_time_to_fall * jump_time_to_fall)) * -1

var jump_allowed_time: float
var pre_jump_allowed_time: float

func enter():
	super()
	jump_allowed_time = jump_buffer
	pre_jump_allowed_time = jump_buffer

func _state_physics_process(delta) -> State:	
	# Set gravity
	controlled_player.velocity.y += fall_gravity * delta
	
	# Start the timer to allow a brief moment to jump when user is at the platform edge
	jump_allowed_time -= delta

	
	# Player still can move when falling
	var direction = Input.get_axis("move_left", "move_right")
	controlled_player.velocity.x = direction * move_speed
	
	# Flip the sprite
	if direction != 0:
		if direction < 0:
			controlled_player.sprite.scale.x = -1
		else:
			controlled_player.sprite.scale.x = 1
	
	# Allow player to press jump button to jump before landing (for a brief moment)
	if Input.is_action_just_pressed("jump"):
		pre_jump_allowed_time -= delta #Start the count down
		if pre_jump_allowed_time > 0 and controlled_player.is_on_floor():
			air_dash.current_air_dash_amount = air_dash.max_air_dash_amount
			return Jump_state
	
	if controlled_player.is_on_floor():
		#reset air dash amount after landing
		air_dash.current_air_dash_amount = air_dash.max_air_dash_amount
		
		if direction != 0:
			return Move_state	
		
		return Idle_state					
	
	if controlled_player.is_on_wall_only():
		#Get wall direction
		var wall_dir = controlled_player.get_wall_normal().x
				
		if ((wall_dir > 0 and Input.is_action_pressed("move_left")) # LEFT wall sliding
		or (wall_dir < 0 and Input.is_action_pressed("move_right"))): # RIGHT wall sliding 
			return Wall_slide_state	
	
	# Move the player
	controlled_player.move_and_slide()
			
	return null	
	
func _state_unhandled_input(event: InputEvent) -> State:
	# Player still can jump when just leaving the edge
	if jump_allowed_time > 0:
		if Input.is_action_just_pressed("jump"):
			return Jump_state	
	if Input	.is_action_just_pressed("shoot"):
		controlled_player._shoot_bullet() 
	if Input.is_action_just_pressed("melee") and Input.is_action_pressed("down"):
		return Slam_down_state
	if Input.is_action_just_pressed("melee"):
		controlled_player._air_slash()	
	if Input.is_action_just_pressed("dash"):
		if air_dash.current_air_dash_amount > 0:
			return Air_dash_state	
	
	return null

An awesome member from reddit found the problem for me so this one is solved.

The physic_process functions in of all my states are called TWICE that I didn’t realize.

A value lesson for me that when a child node called has _process and physical_process functions, they were always called automatically by Godot engine when run

→ My solution is to put

set_physics_process(false)
set_process(false)

in the ready function so that my state machine’s physic process will only be called ONCE by the player only.