• Godot Help
  • Why is clamp being ignored after enough mouse motion?

I've come across a very weird bug for what should be completely straightforward code, I recently made the switch to Godot 4 but I encountered this problem in previous versions as well. If you move the mouse quickly enough vertically, the clamping of the mouse look node somehow breaks and the camera goes past the clamped values. This is a parent node I've made for the camera because I've got other stuff in there as well. Why would the clamp suddenly be ignored in this situation?

Unless I've gone blind, I'm not doing anything particularly radical with my setup.

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		rotate_y(deg_to_rad(-event.relative.x * mouseSensitivity))
		
		mouseLookNode.rotate_x(deg_to_rad(mouseSensitivity * event.relative.y))
		mouseLookNode.rotation.x = clamp(mouseLookNode.rotation.x, deg_to_rad(-80), deg_to_rad(80))

Edit: If it helps just in case here's a screenshot of my hierarchy setup.

  • That video looks like what would happen if the x and y rotations are both performed on the same node.
    For example I can recreate the rolling to the side if I change the code to:

    func _unhandled_input(event):
    	if event is InputEventMouseMotion:
    		rotate_y(deg_to_rad(-event.relative.x * mouseSensitivity))
    		
    		rotate_x(deg_to_rad(mouseSensitivity * event.relative.y))
    		rotation.x = clamp (rotation.x, -(PI/4), PI/4)

    so both x and y rotations happen to the character controller, instead of x to the mouselooknode and y to the controller.

    As long as the character controller only rotates around y and the mouselooknode only rotates around x, that shouldn't happen.
    I'd say try logging out the x,y,z rotations of the Player, MouseLookNode and PlayerCamera then look at what they are when the view is twisted. The Player should have 0 for the x and z, the MouseLookNode should be 0 for y and z, the PlayerCamera should be 0 for everything.
    (I wonder if something is rotating the camera, which is adding to the rotation of the mouse node)

I don't know if this helps, but this is my mouselook code (C#). I do it in _process instead of _unhandled_input, and mousePosDiff is event.relative that i set in _unhandled_input. I clear mousePosDiff at the end of _process. I'm not familiar with the RotateX/Y methods or setting rotation like that, so I'm not sure exactly what they do. I ported my camera controller from my monogame camera controller, and had to find godot methods that matched and this seems to work well.

I don't really know GDScript, but in my opinion your "clamp" can't be ignored, which means that something else is affecting it. That isn't enough sample code to surmise what that might be.

            if (mousePosDiff != Vector2.Zero)
            {


                Yaw -= mousePosDiff.x * CameraSensitivity;
                if (Yaw > 360)
                {
                    Yaw -= 360;
                }
                if (Yaw < 0)
                {

                    Yaw += 360;
                }



                Pitch -= mousePosDiff.y * CameraSensitivity;

                if (Pitch > 90)
                    Pitch = 90;

                if (Pitch < -90)
                    Pitch = -90;

                Transform = new Transform3D(Quaternion.FromEuler(new Vector3(Mathf.DegToRad(Pitch), Mathf.DegToRad(Yaw), 0)), Transform.origin);
                

            }

I think we're going to have to poke at things generally and see what's what because I've been playtesting my controller quite a bit and it's weirdly consistent in how it breaks, one thing that did change though if slightly was when I switched the calculations from deg_to_rad to PI/4. This worked a bit better, but the breaking behaviour still happened if I wiggled the mouse around enough. I have no problem posting the player controller code just bear in mind there is a lot, it's loosely based off Garbaj's FPS tutorial and I've done some heavy tweaking. It's one of the things I want to release as open source since I've been surprised at how little fully functional FPS controllers there are out there with the features you'd expect for Godot. I'm going to throw out some open source templates with controllers so that people can have fun with the engine and not get frustrated over incomplete or broken examples.

extends CharacterBody3D

var mouseSensitivity = 0.2
var direction = Vector3()
var gravityVector = Vector3()
var horizontalVelocity = Vector3()
var movement = Vector3()
var customVelocity = Vector3()
var fall = Vector3()
var speed
var defaultMoveSpeed = 5.0
var sprintMoveSpeed = 10.0
var crouchMoveSpeed = 1.0
var gravity = 10
var jumpHeight = 6
var horizontalAcceleration = 6

var swayAmount = 30
var verticalSwayAmount = 30

var plasmaPistolMaxGunShake = 1.5
var crouchingPlasmaPistolMaxGunShake = 0.5

@onready var mouseLookNode = $MouseLookNode
@onready var playerCollisionShape = $PlayerCollisionShape
@onready var playerCamera = $MouseLookNode/PlayerCamera
#@onready var playerHand = $MouseLookNode/Hand
@onready var playerRaycast = $MouseLookNode/PlayerCamera/PlayerRaycast
@onready var playerCrosshair = $MouseLookNode/PlayerCamera/CenterContainer/PlayerCrosshair
@onready var plasmaBullet = preload("res://PlasmaBullet.tscn")
@onready var plasmaGrenade = preload("res://PlasmaGrenade.tscn")
@onready var grenadeTransform = $MouseLookNode/GrenadeTransform
@onready var flashLight = $MouseLookNode/Flashlight
@onready var raycastCeilingCheck = $RaycastCeilingCheck
@onready var groundCheck = $GroundCheck


@onready var defaultCameraHeight = $defaultCameraHeight
@onready var crouchCameraHeight = $crouchCameraHeight
@onready var playerCrouchingCollisionShape = $playerCrouchingCollisionShape

@onready var fireRateTimer = $FireRateTimer

var isHittingCeiling = false
var isCrouching = false
var isSprinting = false
var isPlayerFiring = false
var isFlashlightEnabled = false

var canEnterDriverSeat = false
var vehicleDriverArea

func _ready():
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	playerCamera.make_current()
	isCrouching = false
	playerCollisionShape.set_deferred(("disabled"), false)
	playerCrouchingCollisionShape.set_deferred(("disabled"), true)
	speed = defaultMoveSpeed
	

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		rotate_y(deg_to_rad(-event.relative.x * mouseSensitivity))
		
		mouseLookNode.rotate_x(deg_to_rad(mouseSensitivity * event.relative.y))
		mouseLookNode.rotation.x = clamp (mouseLookNode.rotation.x, -(PI/4), PI/4)
#		mouseLookNode.rotation.x = clamp(mouseLookNode.rotation.x, deg_to_rad(-80), deg_to_rad(80))
		
func _physics_process(delta):
	
	direction = Vector3()
		
	if Input.is_action_pressed("Crouch") and isCrouching == false:
		isCrouching = true
		playerCollisionShape.set_deferred(("disabled"), true)
		playerCrouchingCollisionShape.set_deferred(("disabled"), false)
		mouseLookNode.global_transform.origin = crouchCameraHeight.global_transform.origin
		speed = crouchMoveSpeed
	elif Input.is_action_just_released("Crouch") and isCrouching == true and raycastCeilingCheck.is_colliding():
		isCrouching = true
		playerCollisionShape.set_deferred("disabled", true)
		playerCrouchingCollisionShape.set_deferred("disabled", false)
		mouseLookNode.global_transform.origin = crouchCameraHeight.global_transform.origin
		speed = crouchMoveSpeed
	elif not Input.is_action_pressed("Crouch") and isCrouching == true and raycastCeilingCheck.is_colliding():
		isCrouching = true
		playerCollisionShape.set_deferred("disabled", true)
		playerCrouchingCollisionShape.set_deferred("disabled", false)
		mouseLookNode.global_transform.origin = crouchCameraHeight.global_transform.origin
		speed = crouchMoveSpeed	
	elif not Input.is_action_pressed("Crouch") and isCrouching == true and not raycastCeilingCheck.is_colliding():
		isCrouching = false
		playerCollisionShape.set_deferred("disabled", false)
		playerCrouchingCollisionShape.set_deferred("disabled", true)
		mouseLookNode.global_transform.origin = defaultCameraHeight.global_transform.origin
		speed = defaultMoveSpeed
		
	
	if not groundCheck.is_colliding():
		gravityVector += Vector3.DOWN * gravity * delta
	else:
		gravityVector = -get_floor_normal() * gravity
		
	if Input.is_action_just_pressed("Space") and groundCheck.is_colliding() and not raycastCeilingCheck.is_colliding():
		gravityVector = Vector3.UP * jumpHeight
		
	if Input.is_action_pressed("Shift") and isCrouching == false and groundCheck.is_colliding() and not raycastCeilingCheck.is_colliding():
		isSprinting = true
		speed = sprintMoveSpeed
	elif Input.is_action_just_released("Shift") and isCrouching == false and groundCheck.is_colliding() and not raycastCeilingCheck.is_colliding():
		isSprinting = false
		speed = defaultMoveSpeed
	
	if Input.is_action_pressed("W"):
		direction += transform.basis.z
	elif Input.is_action_pressed("S"):
		direction -= transform.basis.z
	if Input.is_action_pressed("A"):
		direction += transform.basis.x
	elif Input.is_action_pressed("D"):
		direction -= transform.basis.x
		

	if Input.is_action_just_pressed("E") and canEnterDriverSeat == true:
		vehicleDriverArea.ActivateVehicleCamera()
		queue_free()
		
	if Input.is_action_just_pressed("E") and playerRaycast.is_colliding():
		var playerRaycastCollision = playerRaycast.get_collider()
		if playerRaycastCollision.has_method("SecurityGateButton"):
			playerRaycastCollision.call_deferred("SecurityGateButton")
			
	if Input.is_action_just_pressed("F") and isFlashlightEnabled == false:
		flashLight.visible = true
		isFlashlightEnabled = true
	elif Input.is_action_just_pressed("F") and isFlashlightEnabled == true:
		flashLight.visible = false
		isFlashlightEnabled = false
			
		
	direction = direction.normalized()
	horizontalVelocity = horizontalVelocity.lerp(direction * speed, horizontalAcceleration * delta)
	movement.z = horizontalVelocity.z + gravityVector.z
	movement.x = horizontalVelocity.x + gravityVector.x
	movement.y = gravityVector.y
	set_velocity(movement)
	set_up_direction(Vector3.UP)
	move_and_slide()
	velocity = velocity

OH! I just clicked on something having a glance at my code! It might in fact be to do with my crouch code and me changing the camera origin? I'll double check that and see if it works, but here it all is in case.

Edit: Nevermind, double checked and same results.

For the record, I thought maybe that changing the camera origin in the physics process constantly was potentially screwing up the rotation clamp calculations but that doesn't seem to be the case since commenting it out didn't fix it.

    Lethn I took your code and simplified it to just the mouse look component and I could not get it to print anything out of the range of -.75 to .75 regardless of how fast i moved my mouse:

    extends CharacterBody3D
    
    var mouseSensitivity = 0.2
    var direction = Vector3()
    var gravityVector = Vector3()
    var horizontalVelocity = Vector3()
    var movement = Vector3()
    var customVelocity = Vector3()
    var fall = Vector3()
    var speed
    var defaultMoveSpeed = 5.0
    var sprintMoveSpeed = 10.0
    var crouchMoveSpeed = 1.0
    var gravity = 10
    var jumpHeight = 6
    var horizontalAcceleration = 6
    
    var swayAmount = 30
    var verticalSwayAmount = 30
    
    var plasmaPistolMaxGunShake = 1.5
    var crouchingPlasmaPistolMaxGunShake = 0.5
    
    @onready var mouseLookNode = $MouseLookNode
    @onready var playerCollisionShape = $PlayerCollisionShape
    @onready var playerCamera = $MouseLookNode/PlayerCamera
    
    
    
    
    var isHittingCeiling = false
    var isCrouching = false
    var isSprinting = false
    var isPlayerFiring = false
    var isFlashlightEnabled = false
    
    var canEnterDriverSeat = false
    var vehicleDriverArea
    
    func _ready():
    	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
    	playerCamera.make_current()
    	isCrouching = false
    	playerCollisionShape.set_deferred(("disabled"), false)
    	speed = defaultMoveSpeed
    	
    
    func _unhandled_input(event):
    	if event is InputEventMouseMotion:
    		rotate_y(deg_to_rad(-event.relative.x * mouseSensitivity))
    		
    		mouseLookNode.rotate_x(deg_to_rad(mouseSensitivity * event.relative.y))
    		mouseLookNode.rotation.x = clamp (mouseLookNode.rotation.x, -(PI/4), PI/4)
    		prints(mouseLookNode.rotation.x);
    #		mouseLookNode.rotation.x = clamp(mouseLookNode.rotation.x, deg_to_rad(-80), deg_to_rad(80))
    
    ```		

    That's really interesting, I wonder if there's something going on with the mouse in another part of the code then? I wonder what it could be, I should probably isolate sections with commenting in that case and try to find the exact cause.

    Okay, for the record, I still managed to break the clamp even with this minimal example, what's interesting though about the way it breaks is the rotation borks and goes all wobbly when you're moving the mouse directly up and down, is this an example of some sort of gimbal lock happening possibly? Not sure what to make of it at all.

    It might be easier if you could upload a zip of the project somewhere (or send it to one of us if you don't want it public yet) so we can see it happening live and debug it. I tried some of the code above (in Godot 3.5 and 4) and couldn't get it to fail. It seems like something elsewhere is affecting it after each clamp.

      I'm not sure what you're seeing. It seems pretty consistent here.

      You're not wiggling it enough 😃 you need to wiggle it vertically really. really fast. if that's you.

      It might be easier if you could upload a zip of the project somewhere (or send it to one of us if you don't want it public yet) so we can see it happening live and debug it. I tried some of the code above (in Godot 3.5 and 4) and couldn't get it to fail. It seems like something elsewhere is affecting it after each clamp.

      Kojack I'll upload a vid on my own channel so you can see what's happening, I don't mind sharing the player controller code but the project itself is not something I'm open sourcing.

        Lethn I can assure that I have no interest in anything other than making tiny gremlins walk around procedural terrain populated with crappy tree models, and I only have a middling interest in that.

        If you want my opinion, you have something else going on in your code. Code does what you tell it to, and i've never seen an instance where it decided to do something else. It's pretty unlikely that the clamp is your problem.

        In a pinch you can put some if(....x > abs(80degrees)) prints() messages in different places to debug this, or just put a breakpoint inside of that same if. You'll probably find that it is being affected somewhere else.

        I mean I agree, I just can't isolate what code could be causing it? Either way there's clearly something up with the rotation and it happens specifically when the mouse is wiggled vertically up and down enough, I'll use print and see what's going on a bit in more detail. The thing is I tried commenting out sections and the same thing happened on my end which is pretty odd.

        Lethn It's pretty clear that your "up" axis, or Y rotational basis is getting whacked somehow. I upped the camera sensitivity to .7 and went crazy on the mouse and still could not reproduce.

        But here's a fun bug with my gremlin getting caught on a tree leaf and having it's up axis get jacked up thanks to wonky collision:

        OH! I just thought of something extremely unlikely, could it possibly be my mouse's DPI? I'm going to have to test this.

        Edit: Nope nevermind, still managed it, I do wonder if it's a hardware issue though now I think about it, we need some testers lol.

          Lethn Is there any chance at all you're colliding with something? The point of my video was to show that collisions themselves can affect the Y access of the parent object and screw up the total rotation of the player (of which your camera is attached to). I don't know why shaking the camera like that would do that though.

          That's a good idea! I'll double check but I'm not sure how this could be the case, the camera and mouselook node shouldn't have colliders attached to them unless I've messed up somewhere so it could be a parenting issue? I'll do some poking at it and see what happens.

          That video looks like what would happen if the x and y rotations are both performed on the same node.
          For example I can recreate the rolling to the side if I change the code to:

          func _unhandled_input(event):
          	if event is InputEventMouseMotion:
          		rotate_y(deg_to_rad(-event.relative.x * mouseSensitivity))
          		
          		rotate_x(deg_to_rad(mouseSensitivity * event.relative.y))
          		rotation.x = clamp (rotation.x, -(PI/4), PI/4)

          so both x and y rotations happen to the character controller, instead of x to the mouselooknode and y to the controller.

          As long as the character controller only rotates around y and the mouselooknode only rotates around x, that shouldn't happen.
          I'd say try logging out the x,y,z rotations of the Player, MouseLookNode and PlayerCamera then look at what they are when the view is twisted. The Player should have 0 for the x and z, the MouseLookNode should be 0 for y and z, the PlayerCamera should be 0 for everything.
          (I wonder if something is rotating the camera, which is adding to the rotation of the mouse node)

          ...... You guys are going to love this, it turns out I had my mouse look node's y rotation set to -180 degrees and that's what was causing the clamp issues, I just didn't notice and @Kojack got it right with the MouseLookNode LOL. Amazing how basic things can screw up your code so royally if you haven't done the setup right, thank you, that issue was annoying me and being very baffling because there was nothing in the code I could see that could be doing it.

          Going to have to remember when dealing with my instances to make sure everything's defaulted to 0 before I started placing stuff in scenes. I'll try not to celebrate too soon and make sure to keep testing but holy hell that was frustrating.

          Edit: Yep, confirmed fix now it happens when I rotate the player itself in the scene too, so I need to rotate the scene if I want the player pointing the right way I think that's where I've been scewing up the rotations.