I have a simple character controller controller here and the goal is to make the characters movement speed constant, almost regardless of the underlying surface normal direction (blocking the walking when it hits a certain threshold, like 60° or so-)
The important part of the script is here
func _walking_ground_physics_process(delta: float) -> void:
var movement := Input.get_vector("move_left", "move_right", "move_backward", "move_forward")
if movement.is_zero_approx():
state_chart.send_event("movement_stopped")
return
if !is_on_floor():
print("lost ground")
state_chart.send_event("lost_ground")
var amount_to_move: float = (delta / walk_accel_time) # delta
var movement_speed := movement * walk_speed
movement_speed = movement_speed.rotated(get_viewport().get_camera_3d().global_rotation.y)
# Commented away to test floor velocity
#linear_velocity.x = move_toward(linear_velocity.x, movement_speed.x, amount_to_move)
#linear_velocity.z = move_toward(linear_velocity.z, -movement_speed.y, amount_to_move)
linear_velocity.x = movement_speed.x
linear_velocity.z = -movement_speed.y
It’s most useful to show a video showing the movement of the player
The player clearly refuses to walk up a certain slope, even tho i set constant_speed = true as well as a high wall_mind_slide angle…
But then! when jumping in the direction of the slope (lifting off the ground) and hitting the ground again- then it allows to move upwards. Which can then be denied again when jumping into the direction facing downwards the slope. quite peculiar!
Currently, the player has a single capsule shape as the collider, but i tried it with a sphere and cylinder collider too and that didnt change the behaviour.
Also - you can see in the video how the character is sliding down once I press into the direction of the slope…
this is the current collider I’m using, but I also already tried SphereShape3D and CylinderShape3D and they both worked the same way…
The StateChart plugin works pretty good! -it’s a nice combo of being simple to use but also me understanding how it works internally… which is rare. Previously I wrote my own thing, but StateCharts did everything my thing did, but also has “Gates” which is fun <3
Ok, so looking at your state chart, I see InAir → Landing but also Jump → Fall. My guess is that there is something happening in your state code that when you jump is doing something different than when you don’t.
Looking at your code you have movement_stopped and lost_ground - neither of which conform to names in your state chart which adds to confusion. I recommend that instead of sending movement_stopped you send idle or to_idle. This may help you debug your problem.
If you can’t figure it out, we’ll need to see the full state tree and the attending code.
I’m glad you’re enjoying it. I looked at it, but it didn’t do states the way I want. It uses a “push” model instead of a “pull” model.
i looked more into it, and you are right, the states were confusing things.
So I made a modified version were i completely ripped out all the state logic and just kept the walking state- this is now the entirety of the script
extends CharacterBody3D
class_name PlayerButterfly3
@export_category("Speed")
@export var state_chart: StateChart
@export var walk_speed: float = 0.2
@export var walk_accel_time: float = 1.0
@export var fly_speed: float = 1.4
@export var fly_accel_time: float = 1.0
@export_category("Movement Controls")
@export var can_move: bool = true
@export var can_look: bool = true
@export var rotate_movement_by_active_camera: bool = true
@export var can_jump: bool = true
@export var fly_strength: float = 0.4
@export var jump_linear_velocity: float = 1.0
func _handle_camera_movement():
if can_look:
var motion := Vector2()
motion.x = Input.get_axis("look_left", "look_right")
motion.y = Input.get_axis("look_down", "look_up")
motion += InputBridge.look_delta
motion *= 0.03
%CameraArm.rotation.y -= motion.x
%CameraArm.rotation.x += motion.y
func _process(_delta: float) -> void:
_handle_camera_movement()
var movement := Input.get_vector("move_left", "move_right", "move_backward", "move_forward")
if !movement.is_zero_approx():
movement = movement.rotated(get_viewport().get_camera_3d().global_rotation.y - PI/2.0)
$ButterflyAnchor/monarch_butterfly.global_rotation.y = movement.angle()
var linear_velocity: Vector3
func _physics_process(_delta: float) -> void:
velocity = linear_velocity
on_ground_physics_process(_delta)
_walking_ground_physics_process(_delta)
move_and_slide()
func on_ground_physics_process(_delta: float) -> void:
velocity.y = -1.0
func _walking_ground_physics_process(delta: float) -> void:
var movement := Input.get_vector("move_left", "move_right", "move_backward", "move_forward")
var amount_to_move: float = (delta / walk_accel_time) # delta
var movement_speed := movement * walk_speed
movement_speed = movement_speed.rotated(get_viewport().get_camera_3d().global_rotation.y)
# Commented away to test floor velocity
#linear_velocity.x = move_toward(linear_velocity.x, movement_speed.x, amount_to_move)
#linear_velocity.z = move_toward(linear_velocity.z, -movement_speed.y, amount_to_move)
linear_velocity.x = movement_speed.x
linear_velocity.z = -movement_speed.y
and the same thing still happens, where the character will simply not walk up ledges… and still slides down even when walking up slightly
I didn’t change any other settings.
Soooo I believe this not to be a state issue, but… seemingly a godot issue? it feels like setting “constant_speed” to true would make it move at a constant speed, but no - it goes faster downhill than sideways (not changing height) and despite a high angle setting, the character cannot walk upward… but then it can walk upward when jumping once into the uphill direction, after which it kinda works fine- but the walk speed is still inconsistent…
I just remembered when I was working on a 2D game I had the same issue. The solution was to change the snap_length.
Sets a snapping distance. When set to a value different from 0.0, the body is kept attached to slopes when calling move_and_slide(). The snapping vector is determined by the given distance along the opposite direction of the up_direction.
As long as the snapping vector is in contact with the ground and the body moves against up_direction, the body will remain attached to the surface. Snapping is not applied if the body moves along up_direction, meaning it contains vertical rising velocity, so it will be able to detach from the ground when jumping or when the body is pushed up by something. If you want to apply a snap without taking into account the velocity, use apply_floor_snap().
In 2D I had to change the snap_length from 1px (default) to 10px for a character that was traveling up a 45 degree incline where everything was ~128px square.
By default in 3D, the snap_length is set to 0.1 m. Try setting it to something longer like 1m and see if works, and tweak it up or down until you get the minimum amount that works for your steepest slope.
Unfortunately that also didn’t change a thing. With a full 1 meter snap length, the player was still blocked from moving that direction… BUT!
I figured out a fix, which isn’t a fix. I found one way to combat the “not going in the direction of the slope” issue.
I set the walk speed higher.
Somehow, that lets the character walk up the slope, but even then there is variance between different angles, and the behavior once again changes when you land on the slope while moving in the uphill direction, which somehow always allows to move uphill.
The character does still slide down tho.
To me, this feels like the godot devs optimized the CharacterBody3D for large scale characters, not smol ones like this 20cm butterfly with a 3cm capsule radius collider. It seems to work a lot better with larger values.
The video shows the old one with the state machine, but the behavior was exactly the same with the simplified one, implying that the state really was not the problem
You are correct. That’s a really small character. So here’s a question. Why does it have to be so small? Why not make it so the capsule is a meter in size? It will solve your problems. You can literally just scale it up. The player won’t know the difference.
i generally dont like using LMs for programming, but… here we are.
the problem was that i wasn’t resetting the linear_velocity var i introduced upon landing. this made the character be pushed down constantly, making it slide.