Help with Wall Cover State

Godot Version

4.5.1 Stable

Question

I am working on a Wall Cover mechanic, whereby if you are against the wall and push a button while moving, you will automatically take cover on the wall.

I use state machines for my player’s movement, I have the switch over to my “Wall” state in my “Running State” with the following code under my state machine level _input function;

if player.is_on_floor() and player.is_on_wall() and Input.is_action_just_pressed("testcrouchidle") and player.get_floor_angle() == 0.0:
		lastwall = player.get_wall_normal()
		state_machine.change_state("Wall")

This code works fine, posted just in case it’s relevant to troubleshooting.

Additional Context

player.get_floor_angle() == 0.0 is to check for standing on a slope/stairs to ensure that my player can only take cover on while standing on a flat surface

lastwall is a state machine level Vector3 variable that only gets updated here when switch from Running to Wall state specifically. I will explain the intentions behind lastwall later

Switching to Wall state now, it’s not complete at all, moving on the wall will be its own state that I have not created just yet, but all the dirty work I’m doing here should translate into that state (hopefully). The first thing I wanted to do was to stick the player to the wall, which was easy, using player.velocity.x = move_towards along with player.move_and_slide modifiers to keep player stopped on the wall. I found a circuitous method to flip my player model so that their back is against the wall on both x and z axes.

It “works” but I’m running into some issues. But for now I’ll post the code;

func Enter():
	print("you are in wall cling")

	var wallangle = player.get_wall_normal().normalized().x * (TAU / 2)
	player.basis = player.basis.rotated(player.up_direction, wallangle)
	
	var wallangle2 = player.get_wall_normal().normalized().z * (TAU / 2)
	player.basis = player.basis.rotated(player.up_direction, wallangle2)
	player.basis.orthonormalized()

func handle_input(_event: InputEvent):
	if Input.is_action_just_pressed("testcrouchidle"):
		state_machine.change_state("idle")

func Physics_Update(_delta: float):
	
	var input_vector = Input.get_vector("testrunleft", "testrunright", "testrunforward", "testrundown")
	var _direction := (player.transform.basis * Vector3(input_vector.x, 0, input_vector.y)).normalized()
	
	print(player.get_wall_normal().normalized())
	if player.get_wall_normal().normalized() == Vector3.ZERO:
		player.position += lastwall * -Vector3.ONE
	else:
		player.velocity.x = move_toward(player.velocity.x, 0.0, SPEED)
		player.velocity.z = move_toward(player.velocity.z, 0.0, SPEED)

	
	player.move_and_slide()
	
func Exit():
	var wallangle = player.get_wall_normal().normalized().x * (TAU / 2)
	player.basis = player.basis.rotated(player.up_direction, wallangle)
	
	var wallangle2 = player.get_wall_normal().normalized().z * (TAU / 2)
	player.basis = player.basis.rotated(player.up_direction, wallangle2)
	player.basis.orthonormalized()

Core Issue

When approaching a wall at certain angles, entering Wall state returns a get_wall_normal.normalized() value of Vector3(0.0, 0.0, 0.0), whereas I am looking for either the x value (vertical wall) or the z value (horizontal wall) to be -1.0/1.0. Exiting Wall state with my wall_normal returning Vector3.ZERO will either invert my controls or invert the rotation of my player character (this is a side effect of how my controls are set and the variance depends on whether or not my camera is in a certain position and rotation, a symptom, not the root problem).

I need a way to either avoid getting a Vector3.ZERO returned altogether or a contingency if statement that fixes the problem

Additional Context

So in this code are my attempts at mitigating my wall_normal value returning Vector3.ZERO, which includes my aforementioned lastwall variable, which copies the last wall_normal value of my Running state (and only updates in the transition between Running and Wall). wall_normal values are fine in Running state, always detects a wall (I used the same print command seen above in Running State).

In my state machine level _*physics process, I have attempted to force my player against the wall and see if it returns a valid wall_*normal value, I’ve tried using velocity, position and global_position, increased by the lastwall value, multiplied by negative Vector3.ONE to ensure opposite movement to the correct wall collision, this does not work. I can still get Vector3.ZERO values returned.

I call my rotations twice each upon entering and exiting the state, because I would get the same problem (inverted controls/rotation) even with valid wall_normal values if I only ran the rotation once. I would have to enter Wall state again to fix it. If the rotation script put in _physics_process, i get a funky jittery rotation because it may be interfering with my player _physics_process script which controls rotations based on velocity.

Could also use help with

idk if it’s related or not to the issue, but it’s something I’m going to have to deal with eventually, moving parallel to the wall while up against it returns a valid wall_normal value, which is fine, but the player model only rotates against the direction they are facing when entering Wall state, and of course, I’d like them to put their back to the wall regardless of which direction they are facing so as long as the wall_normal value is valid.
(Sometimes doing this also returns a Vector3.ZERO)

Ah—I see exactly what’s happening here. You’re running into a common issue with Godot’s is_on_wall() and get_wall_normal() in 3D with CharacterBody3D (or a custom Rigidbody2D/3D setup), especially when trying to “cling” to walls at certain angles. Let me break it down carefully and propose a robust solution.


Root Cause

  1. get_wall_normal() only returns a non-zero vector if there is an active collision during that physics frame.

    • If the player is approaching a wall at a shallow angle, or the velocity is extremely small, Godot might not register a collision yet.

    • Also, depending on the collision shape (capsule, box), sometimes only a corner touches, resulting in Vector3.ZERO.

  2. is_on_wall() can return true, but get_wall_normal() might still be zero if the collision is considered too shallow, or if multiple collisions “cancel out” in the normal calculation.

  3. Updating lastwall only during the transition doesn’t guarantee it will be valid every frame if the player is slightly off the wall.


Robust Solution Approach

We can handle this in two ways:


:one: Fallback Using lastwall

You already have a lastwall variable. That’s good. You just need a robust check to ensure it’s never zero:

var wall_normal = player.get_wall_normal().normalized()

if wall_normal == Vector3.ZERO:
    # fallback to last known wall normal
    wall_normal = lastwall
else:
    lastwall = wall_normal

  • This ensures your player always has a valid wall direction, even if get_wall_normal() fails for a frame.

:two: Raycast Method (Recommended for Wall Cling)

Instead of relying purely on get_wall_normal(), cast a short ray from the player toward the last movement direction to detect the wall reliably:

var ray_length = 0.5
var from = player.global_transform.origin
var to = from + -player.transform.basis.z * ray_length  # assuming forward is -Z

var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(from, to, [player])

if result:
    wall_normal = result.normal
    lastwall = wall_normal
else:
    wall_normal = lastwall

:white_check_mark: Advantages:

  • Never returns zero as long as lastwall is set initially.

  • Works even if get_wall_normal() fails due to shallow collisions.

  • Gives you a precise normal to align the player against the wall.


:three: Align Player to Wall

Once you have a valid wall_normal, align the player’s back to the wall:

func align_to_wall(normal: Vector3):
    var target_basis = Basis().looking_at(-normal, Vector3.UP)
    player.basis = target_basis.orthonormalized()

  • -normal because you want the player’s back facing the wall, not the front.

  • orthonormalized() fixes floating-point drift.

  • This is simpler than trying to rotate separately on X and Z axes.


:four: Putting It Together in Your Wall State

func Enter():
    print("you are in wall cling")
    var wall_normal = player.get_wall_normal().normalized()
    if wall_normal == Vector3.ZERO:
        wall_normal = lastwall
    else:
        lastwall = wall_normal
    align_to_wall(wall_normal)

func Physics_Update(_delta: float):
    # get input
    var input_vector = Input.get_vector("testrunleft", "testrunright", "testrunforward", "testrundown")
    var _direction := (player.transform.basis * Vector3(input_vector.x, 0, input_vector.y)).normalized()

    # check wall
    var wall_normal = player.get_wall_normal().normalized()
    if wall_normal == Vector3.ZERO:
        wall_normal = lastwall
    else:
        lastwall = wall_normal

    # stick to wall
    player.velocity.x = move_toward(player.velocity.x, 0.0, SPEED)
    player.velocity.z = move_toward(player.velocity.z, 0.0, SPEED)

    align_to_wall(wall_normal)

    player.move_and_slide()


:white_check_mark: Key Notes

  1. Never rely on get_wall_normal() alone for wall cling; always have a fallback like lastwall or a raycast.

  2. Use Basis().looking_at(-normal, Vector3.UP) to align your player’s back to the wall in a single step.

  3. Avoid rotating separately on X and Z axes unless you really need to; it introduces jitter.

  4. Update lastwall every physics frame if a valid normal exists, not only during the transition. This fixes the zero-normal issue.

yeees the em dash

what do you mean?

sorry I just saw it and had to point it out lol the ‘—’ at the beginning of the response is a sign of LLM text. I didn’t mean anything by it, it just was the first time I noticed something like that.

Hello, thank you for your response! I attempted your solution, I still get Vector3.ZERO values on the get_wall_normal, and due to the new basis rotation, it rotates my model/controls even on valid values, and the player is not aligning against the wall correctly either.

I’m willing to redo the state entirely and try with a raycast, if you’re correct in it ignores shallow collisions. I feel like falling back on lastwall is not working correctly.

Do I need a collision shape node on my CharacterBody3D or is this something done all thru code? I’m willing to do whichever is simpler.

Sorry for not answering before but from what I research it seems get_wall_normal() only happens when the collision to the wall is perfect which is probably why you are getting Vector3.ZERO sometimes.

Using a raycast either from where the player is looking or direction their facing (depends if your game is third person or first person) will allow you to get the wall_normal every frame the player is looking at it. I feel like that would be more robust. It even adds the bonus of adding it to another collision layer so that feature can be isolated and not interfere with other feature you may have. Also if you don’t need to every frame you can put it on a AutoStart Timer (its good if your raychecks cause performance issues)

There are 2 method to do raycast3D, use the Node provided by the engine (easier) (recommended unless you have a very specific reason to use scripting Raycasts.)

or

Use scripting to do your checks

I’m fairly new to gdscript, what are the best practices for using a RayCast3D node to detect walls, I’m aware of the is_colliding check, but what do I do with it? Do I still need to use the get_wall_normal in conjunction with it?

Can I use it to handle rotations (putting my character’s back to the wall)?

Do I need to use the RayCast3D to detect the wall instead of player.is_on_wall in my Running State?

I’ll think it over and check back when I’m off of work today, I appreciate any help you or anyone else can provide.

So with the Raycast3D node there is a method callled get_collision_normal() so you can do something like this

@export var raycast_ref: Raycast3D

func _physics_process(delta: float) -> void:
	if raycast_ref.is_colliding():
		var raycast_normal_collision = raycast_ref.get_collision_normal()

then use raycast_normal_collision for anything you need

there is also a better way to reference other nodes in the same scene (personally):

when you right click a node (not the root/top-parent) and select ‘Access as Unique Name’ you can reference the node using what it is named lets say the node is named ‘wall_check_raycast’ then you can reference it like this instead of using @export

func _physics_process(delta: float) -> void:
	if %wall_check_raycast.is_colliding():
		var raycast_normal_collision = %wall_check_raycast.get_collision_normal() #value is changed here
#this only works if you right click and turn on Access as Unique Name on the Node

that could become a bit messy on bigger projects so doing this is good practice if you know the project will grow

var raycast_ref: Raycast3D = %wall_check_raycast

func _physics_process(delta: float) -> void:
	if raycast_ref.is_colliding():
		var raycast_normal_collision = raycast_ref.get_collision_normal() 

this way if you ever rename your nodes you will only have to change it in 1 place instead of having to go through you code and change every instance of it.

OK, I followed what you said to set up my RayCast node, I ran into a lot of issues at first until I realized that I rotate my Armature and not the whole CharacterBody3D so making the RayCast a child of the armature fixed that and now the raycast will detect walls, I’ve removed most player.is_on_wall() checks in place of the raycast collision normal. This has fixed all problems pertaining to getting Vector3.ZERO returns and having it mess up my Wall State and exits from my Wall State!

In Running State transition to Wall State

if player.is_on_floor() and player.is_on_wall() and Input.is_action_just_pressed("testcrouchidle") and player.get_floor_angle() == 0.0 and wallray.is_colliding():
	lastwall = wallray.get_collision_normal()
	state_machine.change_state("Wall")

Just in case, I kept my lastwall variable around and matched it with the raycast instead of floor_normal. As of this moment, I don’t have a use for it quite yet.

Context for First Problem

I have now ran into the issue of putting my player’s back to the wall, after a lot of troubleshooting, I found that the original way I wrote the code had the closest results I wanted, I updated it to reflect the RayCast Node and is as follows;

Fig1

	var wallangle = wall_detect.get_collision_normal().normalized().x * (TAU / 2)
	var wallangle2 = wall_detect.get_collision_normal().normalized().z * (TAU / 2)
	
	player.basis = player.basis.rotated(Vector3.UP, wallangle)
	player.basis = player.basis.rotated(Vector3.UP, wallangle2)
	player.basis.orthonormalized()

It’s still being run twice each for entering and exiting wall state, but here’s the problems that persist:

First Problem

I still can’t get perfect “back to wall” regardless of the angle I approach the Wall to enter Wall State, now that running parallel to the wall isn’t enough to enter Wall State anymore (on account of the raycast on a -Z front facing armature being the only check for walls), I don’t have to worry about getting obtuse angles, but it’s still an issue I need help with.

Context for Second Problem

I did some edits to my CharacterBody3D root script to help with rotations while running, before I used armature rotations using the following;

Fig2

if velocity.length() > 0.0:
	var look_direction : Vector2 = Vector2(-velocity.z, -velocity.x)
	armature.rotation.y = look_direction.angle()

It was crude, but for a while was sufficient in this prototype I’m working up. There was a side effect, since the angles were dependent on velo, whenever my player hits a wall, the respective velo hits 0, which in effect means that this code prevents my player from ever facing the wall if they’re moving into it, they will always face parallel to the wall in this case. Now obviously that presented an issue for raycasts that wants to detect a wall when facing one and close enough to one that get_wall_normal could return valid values. So I had to update my script, it is now;

Fig3

	var input_vector = dirpipe
	var direction:= (transform.basis * Vector3(input_vector.x, 0, input_vector.y)).normalized()

	if direction: 
		if cam.rotation_degrees == Vector3(-90.0, 0, 0):
			var visual_dir = transform.basis * (Vector3(input_vector.x, 0, input_vector.y)).normalized()
			visuals.rotation.y = lerp_angle(visuals.rotation.y, atan2(visual_dir.x, visual_dir.z), _delta * SMOOTH)
		else:
			var visual_dir = cam.transform.basis * (Vector3(input_vector.x, 0, input_vector.y)).normalized()
			visuals.rotation.y = lerp_angle(visuals.rotation.y, atan2(-visual_dir.x, -visual_dir.z), _delta * SMOOTH)

As you can see my armature in the Characterbody3D script is now named visuals

SMOOTH is a float variable set at 10.0 just to smooth out the rotations

dirpipe is a Vector2 variable, my movement is state machine level so in my Running State, I take my input_vector variable from my running state, which has the input detection (Input.get_vector), and I match it up with dirpipe

(in the running state script): player.dirpipe = input_vector

This gives me more control over the Vector2 value in the CharacterBody3D root script, if it were to read inputs directly, all movement would be bound by this rotation, including movement that I would not want to be bound to it, like a dash or eventually when my player will move along the wall they’re taking cover on etc.

There is an @onready var for my camera and that is cam, I found that in Godot, if you use camera.transform.basis, your Z movement is reverse proportional to the Camera3D’s X rotation, if the camera is looking at the player from a 90 degree top down angle, the Z movement gets locked entirely. I have code in my Running State that accounts for that camera position (as my camera is dynamic and changes angles depending on area3d checks, including top down perspective viewpoints) and because the movement checks for the camera, the rotations have to as well, that’s the story behind the if cam.rotation_degrees == Vector3(-90.0, 0, 0):

(I’m not adept at godot, gdscript nor game math enough yet to know why Fig2 code didn’t have to worry about this, but it didn’t)
edit thinking about it, it’s probably because Fig2 was velo dependent and velo is fairly global (provided the camera fixes are utilized), hard vectors are trickier to do the math on lol. We can all grow.

This code allows me to freely face the direction my player is moving, whether or not they are moving into a wall. Lastly, mostly for testing/debug purposes, but also because I wanted to do this eventually, I allowed my player to enter Wall State from an Idle State, it’s running the same checks as in the Running State, except in Idle State, I will not check for is_on_wall(), as standing still on a wall is what can get Vector3.ZERO values, so the key check is all on raycasts.

In Idle State transition to Wall State

if player.is_on_floor() and Input.is_action_just_pressed("testcrouchidle") and player.get_floor_angle() == 0.0 and wallray.is_colliding():
	lastwall = wallray.get_collision_normal()
	state_machine.change_state("Wall")

Second Problem

This is all context that could help explain the second problem I’m having, when on a top down angle, when moving into the wall and entering Wall State from Running State, my player makes a complete 360 degree rotation (facing the same direction, but actually spinning around) when on the wall. This does not occur from the Idle State under a top down camera, nor does it happen coming from neither Idle nor Running to Wall State when the camera is in what I call “normal positions” (basically anything not at `cam.rotation_degrees == Vector3(-90.0, 0, 0)`)

Both problems could be related, because I attempted to replace all instances of player.basis to armature.basis in the Wall State and I recreated the same issue but also for when camera is in normal positions, and that tells me that it has to do with how I’m working these rotations. Please advise! :smile:

Thank you for your time!

One thing I need for context is what amount of control does the player have with the camera?

Addressing the first problem:

I would need more information on what the issue looks like, I can’t really imagine what is happening that is the issue, is the player not getting stuck to the wall or is the player able to rotate while against the wall.

The vector math you provided [wall_detect.get_collision_normal().normalized().x * (TAU / 2)] seems maybe cause issues, it would be better to combine this with the wall normal to figure out which direction you want to be away from:

var normal: Vector3 = wallray.get_collision_normal
var look_at_point: Vector3 = normal + player.global_position
player.look_at(look_at_point, Vector3.UP) 
#look_at is better to try to figure out how to use that rotated in this context

Problems that arise with this Vector Math (not something to worry about until you fix your other issues):

Now the main problem is when you use this vector math is the Raycast3D will now rotate away from the wall and there will be no normal to get for this math, now lets say you want the player to stick to a wall corner to another corner, this wont work since there is not other normal to bind to. (unless your game doesn’t have this)

A potential solution is to have another raycast3D check that detects what walls are nearby (you would have to use multiple rays) and depending on player input switch which wall is the wall to bind to.

Addressing the second problem:

I need to see the node structure for more context, I don’t know what could be the issue with just what was provided

This video isn’t exactly what you are doing but understanding the code and what is it doing could help you figure out what type of math you would need

Here is a layout of my scene. I will provide a video momentarily, but I would have to revert some code back in to demonstrate the issue, as of this moment there is no rotation code in because I feel like I’m at my wit’s end with it. Your latest code did not work either, and the video provided didn’t prove a helpful reference because it’s not fixing the problem, in fact using position/global_position values at all seems to snap my player character to a physical point in the level scene. and the look_at isn’t doing the job either.

Offscreen is a Timer Node that is only used by my Tackle state, and that’s everything.

Sticking to the wall was never the problem, we can do that just fine, or at least simulate it by just having the move_toward math applied to velocity. Physically sticking to the wall is tricky because we’ve discovered that standing still next to a wall can confuse godot into thinking that you’re not actually/technically touching the wall (and thus it returns Vector3.ZERO in the get_wall_normal). I tried doing that in my OP code where you see-

if player.get_wall_normal().normalized() == Vector3.ZERO:
		player.position += lastwall * -Vector3.ONE

I need:

A way to detect the angle that my player is approaching a wall
A way to calculate and apply the rotation to either my player or their armature so that their back is always parallel to the wall they are on (using the wall angle) while facing away from said wall (ensuring that my player’s Vector3.FORWARD is perfectly perpendicular to the wall, facing opposite it), to recap, we’ve tried rotating along the basis, a look_at command, and just brute forcing a player.rotation_degrees.

And I need it to work independent of camera position/rotation (or at least account for it).

At the moment the player has no control over the camera. In the future I’m considering an old school first person free camera (where you can only use it while standing still/in the Idle State).

My basis/wallangle/wallangle2 code at least produced a 180 degree rotation (except in the specific instance where they do a full 360 for some reason), but we’re shy of fully functional because I don’t know how to get the angle from the wall right to make sure the game knows how much to rotate to ensure “back parallel to wall”.

I’ll get to work on reproducing the issue and recording it so you have a visual reference point.

edit, here it is!

Thanks again for your time!

(I also do not plan to allow the player to switch to a different wall automatically, the game will not necessitate it)

Thanks for the video if you click ‘Debug’ and click ‘Visual Collision Shapes’ how does the raycast behave?

So I think that my thinking from before was wrong looking at the problem with fresh eyes.

Looking at this from a perspective of I need to rotate the player this much to match the wall is what is causing most of the frustration. We can simplify it.

Instead if you use the walls data to simply snap the player to the wall depending on the normal and use the normal as the snapped state and then do the rest of the wall state, it would be better:

func align_to_wall(): -> void:
	var wall_normal: Vector3	
	if wallray.is_colliding():
		wall_normal: Vector3 = wallray.get_collision_normal()
	else: #i feel like last wall is used to mask a problem with your state transitions
		wall_normal = lastwall
	player.look_at(player.global_position + normal, Vector3.UP)
	visuals.rotation = Vector3.ZERO

now if you use this function once while transitioning to wall state and make sure to NOT update the wall_normal after it has been aligned. (I don’t know if I have it 100% right)

I think the reason the wall normal changes so much is because of the wallray rotating with the visual (see it easier with Debug > Visual Collision Shapes) and then in the code updating the wallray after the visual have rotated, causing your state to think that it is off the wall.

Let me know what helps you from this, if you do need more help I need a re context of everything that we said because it is a lot to go back and read previous code.

I wish you luck! :saluting_face:

So the RayCast works just fine, Debug > Visible Collision Shapes verified what I knew (my Sprite3D node was used to make sure my RayCast worked properly), it’s working fine.

Your code does not accomplish the desired rotation, unfortunately. I find that using look_at rotations permanently rotates my CharacterBody3D, which alters the local armature rotations, Vector3.FORWARD becomes opposite whatever wall i cling to. This is fascinating, because I still can’t get perfect “back to wall” without facing perpendicular to the wall, but the rotation of the CharacterBody3D is perfectly along the axis of the wall itself.

For example, facing perpendicular towards the wall is considered approaching it at 0 degrees, I get a perfect 180 rotation with your code (as could I in my old twice run rotation code), at this angle, we get perfect back to wall, facing perpendicular away from the wall.

For Context

	var wallangle = wall_detect.get_collision_normal().normalized().x * (TAU / 2)
	var wallangle2 = wall_detect.get_collision_normal().normalized().z * (TAU / 2)
	
	player.basis = player.basis.rotated(Vector3.UP, wallangle)
	player.basis = player.basis.rotated(Vector3.UP, wallangle2)
	player.basis.orthonormalized()

When I was using get_wall_normal, I could enter Wall State approaching the wall at 90 degrees (running parallel to the wall while up against it). Now using just the RayCast, my range is limited to closer to -65 degrees to 65 degrees, approaching the wall at any angle will make my player rotate 180 degrees on the spot (including the 90 degrees under get_wall_normal).

Both of our code has the aforementioned 360 rotation problem as well, no fix there yet either.

However with your code, my player basis also now shifts a perfect 180 degrees from the wall, no matter which angle I approach it.

Something about the calculations of look_at or look_at itself is getting the mathematical results that I want but it’s being applied somewhere I don’t want it to. Or maybe it’s because of how this rotation works, “feature not a bug” type stuff. Either way, it made me feel like if I think about this enough an idea could sprout.

Alas, it creates a new problem because my armature now is not rotating correctly either, under a top down camera, my controls are affected too, as “forward” becomes away from the wall, whichever wall I first enter Wall State on.

It does not change after the first time.

lastwall wasn’t intended to be a mask, more of a failsafe because of the problem with getting Vector3.ZERO returned on get_wall_normal, It doesn’t really have a bonafide use quite yet, it’s depreciated because we’re not trying to use or rely on get_wall_normal. And I had since changed it to = wallray.get_collision_normal(). I think that would have made your code redundant, so I did change it back to player.get_wall_normal()when using your code. Let me know if I shouldn’t have.

It’s apparently possible to capture the axis between wall and player, but through the look_at it’s coming with consequences, and it’s not being applied to the initial rotation either.

To recap, the full context of what I need at this point in time.

RayCast3D Node fixes my initial problem with relying on get_wall_normal, beforehand I would occassionally get Vector3.ZERO returned to it, which created problems with my Wall State.

As a result of using a RayCast3D Node, I had to update my movement based rotation code, which is in my CharacterBody3D root script, it reads inputs from my Running State instead of reading inputs directly as to not influence other States as the rotations here are based on input vectors, not velocity, as it was before.

What I need, first priority.

A way, anyway to get my player to rotate in a way that always puts my player facing perpendicular away from the wall while in Wall State, and only in Wall State.

From there we can navigate other problems, I think that my other issues are downstream of this initial need and that getting this figured out first and foremost is the way to go (and could potentially fix the other problems on its own).

Additional Factor(s)

I have a dynamic camera system, that changes angles and behavior depending on Area3D checks in my Scene Tree, this makes having camera based movement necessary camera.transform.basis
However with this, my Z movement becomes reverse proportional to the Camera’s X rotation, a top down viewpoint with the CharacterBody3D Node in the middle completely locks moving forward or backward (you can test this yourself in godot). So additional code was added to the movement script in my Running State to account for this. I orthonormalized the camera.basis and set the camera.basis.y = Vector3.UP, this fixes the issue, and because of the -90 degrees, it will invert the controls at that angle, so I include an if statement for the camera being at that rotation, and when it applies, I use my player.transform.basis to move, in it, I invert the input_vector to account for the inverted controller.

	camera_basis.y = Vector3.UP

	camera_basis = camera_basis.orthonormalized()

	var direction := (camera_basis * Vector3(input_vector.x, 0, input_vector.y)).normalized()
# Alternative direction specifically for -90 degree x rotated camera
	var directiontop := (player.transform.basis * Vector3(-input_vector.x, 0, -input_vector.y)).normalized()
	
	# Conditional movement depending on top down view or not
	if cam1.rotation_degrees == Vector3(-90.0, 0.0, 0.0):
		player.velocity.x = directiontop.x * SPEED
		player.velocity.z = directiontop.z * SPEED
		
	else:
		player.velocity.x = direction.x * SPEED
		player.velocity.z = direction.z * SPEED

This creates a situation that the rotation math may or may not have to account for. In my old basis rotations, I encounter a 360 degree turn entering Wall State from Running State when my camera is top down, It can be recreated with your code as well in all camera angles.

I consider these two camera angles to be the only POVs used, despite the dynamic nature of the camera. I categorize them as “top down” and “normal positions”.

The rotations have an effect in both camera perspectives, the difference is just due to the transform.basis. Camera basis only affects my armature rotation, Player basis affects armature rotation and movement direction. Usually I can tell when something needs to account for the camera when it works perfectly in one POV but not the other. In this case, the problems persist in both POV for both of our code (and any code I’ve attempted to use prior in this thread), the symptoms are just different based on which transform.basis is in effect.

I’ll sleep on it and see if I could think of something. I never thought it would be this difficult haha!

Ya I’ll look at this again tomorrow and see if I can spot anything new.

Sometimes its better to work on other systems then come back to this when you feel like it, spending too much time on a problem can cloud a solution that might be sitting in front of your face.

It feels like we are getting there bit by bit

1 Like

Is there a way to reset or toggle the look_at rotation? I’m brainstorming something but I’m not sure.

you would have to create the toggle through code using look_at()

example:

var wall_mode: bool = false

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("wall_state_player_input") && wall_state_change == false:
		align_to_wall()
		wall_state_change = true

func _physics_process(delta: float) -> void:
	if wall_state_change = true:
		#change to wall state however you want here
		wall_state_change = false

if you try to restructure the state into this way it will only happen once then you can do whatever you need to do to make the state work after the alignment

I come bearing good news! I have fixed the issue!

After days of thinking, theorizing and trying new things, the idea hit me that I could be approaching this wrong. Getting too worried about capturing a rotation or a rotation angle hit me hard and I ran into a wall, for a while I was thinking I could use the rotate_toward method, or hard code a mathematic rotation, but the solution was simple.

I started by making a new function in my Wall State

func new_rotation() -> void:
	var wallnormal = wall_detect.get_collision_normal()
	var wallgrav = Vector3(12, 0, 12)
	var backto = wallgrav * -wallnormal
	
	if wallnormal.z != 0.0:
		player.velocity.z += backto.z
		player.velocity.x = move_toward(player.velocity.x, lastwall.x * 0.0, SPEED)
	if wallnormal.x != 0.0:
		player.velocity.x += backto.x
		player.velocity.z = move_toward(player.velocity.z, lastwall.z * 0.0, SPEED)

This func is half the solution, I redid the way my character “sticks” to the wall, now when entering wall state, I apply gravity to the wall itself! Now my player is actually stuck to the wall, specifically along the axis of the wall itself. And now the coup de grace!

I went back and applied my old movement rotation specifically to this wall state, with one slight modification. In my Wall State’s _physics_process, I have this in there now.

new_rotation()
if player.velocity.length() > 0.0:
	var look_direction : Vector2 = Vector2(player.velocity.z, -player.velocity.x)
	armature.rotation.y = -look_direction.angle()

Remember above, this rotation is:

A. Based on Velocity (which here with new_rotation() is captured/set by the new wall sticking gravity)

and

B. Camera independent! This rotation code doesn’t have to account for my camera system!

Setting the rotation to negative look_direction gets the perfect “back to wall”, “looking opposite the wall” regardless of the angle I approach the wall.

This mission is a success~!

Most important lesson I learned: When rotating the armature, don’t apply rotation to the Character Body 3D, one or the other, but not both (for what I’m trying to accomplish).

I’d like to thank both of you for trying to help me and encourage me to work through this. I feel like I became a slightly smarter developer for this.

May this thread serve as an archive for future generations, I haven’t seen anyone else attempt a Wall Cover in Godot 4.5 3D, so here’s to spreading the knowledge!

I’ll be back next time I’m dizzy from trial and error!

2 Likes

Congrats on figuring it out! Good luck on the rest!

1 Like