Camera controller script with collider

I try to finish Unity Essential tutorial, I finished 3D scene with collectibles, even created some shaders with so-called Shader Graphs and some help from Google Gemini, because I never used any shader-language of programming before.


Then I tried to finish the gallery room scene, I manage to crate some materials in Blender 3D (I am bad in Blender), downloaded some textures, I have many troubles with importing of FBX files to Unity, but in the end I obtained gallery that looks not like a total garbage

but now I need camera which movement will be restricted by borders of gallery, so it cannot fly through walls. For this purpose I with help of Google Gemini modified the proposed script of camera controll, but then I obtained kind of flicker, then some errors of rotation and so on, so I need some explanmation what I do wrong…

using UnityEngine;

public class SimpleCameraControllerCollider : MonoBehaviour
{
    // FIELDS

    public float moveSpeed = 3.0f;           // Movement speed
    public float rotationSpeed = 100.0f;      // Rotation speed for A/D keys in degrees per second
    public float mouseSensitivity = 1.0f;    // Mouse look sensitivity

    float _cameraRotationX = 0.0f;
    float _playerRotationY = 0.0f;
    float _rotationX = 0.0f;
    float _rotationY = 0.0f;
    float _rotationZ = 0.0f;
    float _currentCameraRotationX;
    float _currentPlayerRotationY;
    float _cameraRotationXVelocity;
    float _playerRotationYVelocity;
    float _rotationSmoothTime = 0.12f;

    CharacterController _playerCharacterController;
    Transform _playerTransform;

    // METHODS

    void Start()
    {
        _playerCharacterController = GetComponentInParent<CharacterController>();
        _playerTransform = transform.parent; // Get transform component from parent object

        if (_playerCharacterController == null)
        {
            Debug.Log("Character controller component is null");
            return;
        }

        if (_playerTransform == null)
        {
            Debug.Log("Transform controller component is null");
            return;
        }

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    void Update()
    {
        // MOUSE

        // Mouse Look (added smoothness)
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;

        _cameraRotationX -= mouseY;  // Subtracting to invert the up and down look
        _cameraRotationX = Mathf.Clamp(_currentCameraRotationX, -80f, 80f);  // Clamp the up and down look to avoid flipping

        // Smooth rotation using SmoothDamp() method
        _currentCameraRotationX = Mathf.SmoothDamp(_currentCameraRotationX, _cameraRotationX, ref _cameraRotationXVelocity, _rotationSmoothTime); // Rotate camera locally

        // Quaternion to avoid flicker
        _playerTransform.rotation = Quaternion.Euler(_currentCameraRotationX, _rotationY, _rotationZ);

        // KEYS

        // Movement
        float moveForward = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime;

        // Rotation with A/D keys
        float turn = Input.GetAxis("Horizontal") * rotationSpeed * Time.deltaTime;

        // Translate only on the X-Z plane (global Y remains unchanged)
        Vector3 moveDirection = _playerTransform.forward * moveForward;
        _playerCharacterController.Move(moveDirection);

        _playerRotationY += turn;

        _currentPlayerRotationY = Mathf.SmoothDamp(_currentPlayerRotationY, _playerRotationY, ref _playerRotationYVelocity, _rotationSmoothTime);

        _playerTransform.rotation = Quaternion.Euler(_rotationX, _currentPlayerRotationY, _rotationZ); // Rotate around the global Y axis
    }
}

You can add an empty gameObject as a child of your camera and place is slighlty behind or where whever you like, add collider to it and it should work. You can also modify the camera movement via script,

onCollide{
if(col.gameObject.tag=="wall")
{
         RestrictCameraMovement();
} 
}

Imo you should use cinemachine for that issue, it works perfectly.
Hope this helps :slight_smile:

After a couple of days of madness with this motion, rotation and colliders I understand:

  1. If I have some jitter, choppiness et cetra in Game Mode, it can complitelly dissapear in WebGL build. So I just ignored it…

  2. I cannot resolve the problem with corner colliders.

My current script in Camera Object is something like that

using UnityEngine;

// Program to control simple camera with collider on desired height, with help of Mr. Gemini (AI Google)
public class CombinedCameraController : MonoBehaviour
{
    // FIELDS

    [SerializeField]
    float _mouseSensitivity = 2.0f;
    [SerializeField]
    float _rotationSpeed = 100.0f;
    [SerializeField]
    float _smoothTime = 0.5f;
    [SerializeField]
    float _moveSpeed = 3.0f;
    [SerializeField]
    float _raycastOffset = 0.2f;
    [SerializeField]
    float _moveDistanceOffset = 0.3f;
    [SerializeField]
    float _clampRotationDegree = 80f;
    [SerializeField]
    float _minMoveMagnitude = 0.001f;
    [SerializeField]
    float _cameraRotationX = 0f;
    [SerializeField]
    float _playerRotationY = 0f;
    [SerializeField]
    float _triggerColliderDetectionRadius = 0.5f;
    [SerializeField]
    Vector3 _lastPosition;

    Rigidbody _rigidbody;
    Transform _playerTransform;
    Quaternion _cameraInitialLocalRotation;

    // METHODS

    // This method calls before the first frame

    void Start()
    {
        _playerTransform = transform.parent;

        _rigidbody = GetComponentInParent<Rigidbody>();

        if (_playerTransform == null)
        {
            Debug.LogError("Parent Transform not found!");

            enabled = false;

            return;
        }

        if (_rigidbody == null)
        {
            Debug.Log("Initialization ERROR! Parent Rigidbody component cannot be found.");

            enabled = false;

            return;
        }

        _cameraInitialLocalRotation = transform.localRotation;

        _rigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
        //_rigidbody.isKinematic = true;
        _rigidbody.useGravity = false;
        _rigidbody.freezeRotation = true;

        _lastPosition = _rigidbody.position;

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    // This method calls per frame

    void FixedUpdate()
    {
        HandlePlayerRotation();
        HandleCameraRotation();
        HandlePlayerMovement();

        //PreventPushback();

        //_rigidbody.linearVelocity = Vector3.ClampMagnitude(_rigidbody.linearVelocity, _moveSpeed);
    }

    // Only left and right

    public void HandlePlayerRotation()
    {
        // Player Rotation (Y-axis Rotation - A/D keys)
        float horizontalInput = Input.GetAxis("Horizontal");
        _playerRotationY += horizontalInput * _rotationSpeed * Time.deltaTime;

        // Smooth player rotation using Quaternion.Slerp (Spherical-linear interpolation)
        Quaternion targetPlayerRotation = Quaternion.Euler(0f, _playerRotationY, 0f);
        _playerTransform.localRotation = Quaternion.Slerp(_playerTransform.localRotation, targetPlayerRotation, _smoothTime);
    }

    // Only back and forward

    public void HandlePlayerMovement()
    {
        float movePerFrame = _moveSpeed * Time.fixedDeltaTime;

        // Store 'forward' vector before rotation
        Vector3 currentForward = _playerTransform.forward;

        float verticalInput = Input.GetAxis("Vertical");
        //Vector3 moveDirection = _playerTransform.forward * verticalInput * _moveSpeed * Time.deltaTime;

        Vector3 moveDirection = _playerTransform.forward * verticalInput;

        

        if (moveDirection.magnitude > _minMoveMagnitude)
        {
            //    Vector3 adjustedDirection;

            //    if (CheckForWallCollision(moveDirection, out adjustedDirection))
            //    {
            //        _playerTransform.Translate(adjustedDirection, Space.World);
            //    }
            //    else
            //    {
            //        _playerTransform.Translate(moveDirection, Space.World);
            //    }

            Vector3 adjustedDirection;

            if (CheckForWallCollision(moveDirection, out adjustedDirection))
            {
                _rigidbody.MovePosition(_rigidbody.position + adjustedDirection);
            }
            else
            {
                _rigidbody.MovePosition(_rigidbody.position + moveDirection);
            }

            //moveDirection.Normalize();
            //moveDirection *= movePerFrame;

            //_lastPosition += moveDirection; 

            //_rigidbody.MovePosition(_rigidbody.position + moveDirection);
        }
    }

    // Only up and down

    public void HandleCameraRotation()
    {
        // Mouse Look (Camera X-axis Rotation)
        float mouseY = Input.GetAxis("Mouse Y") * _mouseSensitivity;
        _cameraRotationX += mouseY;
        _cameraRotationX = Mathf.Clamp(_cameraRotationX, -_clampRotationDegree, _clampRotationDegree);

        // Smooth camera rotation using Quaternion.Slerp (THIS IS THE KEY by Mr. Gemini)
        Quaternion targetCameraRotation = _cameraInitialLocalRotation * Quaternion.Euler(_cameraRotationX, 0f, 0f);
        transform.localRotation = Quaternion.Slerp(transform.localRotation, targetCameraRotation, _smoothTime);
    }

    public bool CheckForWallCollision(Vector3 moveDirection, out Vector3 adjustedMoveDirection)
    {    
        adjustedMoveDirection = moveDirection;

        Vector3 raycastOrigin = _playerTransform.position + moveDirection.normalized * _raycastOffset;

        bool isCollider = Physics.Raycast(raycastOrigin, moveDirection, out RaycastHit hit, moveDirection.magnitude + _raycastOffset);

        if ((hit.collider != null) && !hit.collider.isTrigger) // Only adjust movement if hit non-trigger collider
        {
            float moveDistance = hit.distance - _raycastOffset;

            if (moveDistance > 0f)
            {
                adjustedMoveDirection = moveDirection.normalized * moveDistance;

                return true;
            }
        }

        return false;
    }

    public Collider[] HandleTriggerDetection()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position, _triggerColliderDetectionRadius);

        foreach (Collider collider in hitColliders)
        {
            if (collider.isTrigger)
            {
                Debug.Log("Trigger entered: " + collider.name);
            }
        }

        return hitColliders;
    }

    public void PreventPushback()
    {
        //if (Vector3.Distance(_rigidbody.position, _lastPosition) > 0.001f)
        //{
        //    _rigidbody.position = _lastPosition;

        //    _rigidbody.linearVelocity = Vector3.zero;
        //}

        //_lastPosition = _rigidbody.position;

        //_lastPosition = _rigidbody.position;

        _rigidbody.MovePosition(_lastPosition); 
    }
}

MovePosition is supposed to be used for moving kinematic rigidbodies and isn’t made for moving objects that need to be deflected by collisions. Instead you can let the physics engine take care of the collisions with walls:

using UnityEngine;

// Program to control simple camera with collider on desired height, with help of Mr. Gemini (AI Google)
public class CombinedCameraController : MonoBehaviour
{
    // FIELDS
    [SerializeField]
    float _mouseSensitivity = 2.0f;
    [SerializeField]
    float _rotationSpeed = 100.0f;
    [SerializeField]
    float _moveSpeed = 3.0f;
    [SerializeField]
    float _moveDistanceOffset = 0.3f;
    [SerializeField]
    float _clampRotationDegree = 80f;
    [SerializeField]
    float _cameraRotationX = 0f;

    Rigidbody _rigidbody;
    Transform _playerTransform;

    void Start()
    {
        _playerTransform = transform.parent;

        _rigidbody = GetComponentInParent<Rigidbody>();

        _rigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;

        _rigidbody.useGravity = false;
        _rigidbody.freezeRotation = true;

        _rigidbody.drag = 3;

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    void Update()
    {
        HandlePlayerRotation();
        HandleCameraRotation();
    }

    void FixedUpdate()
    {
        HandlePlayerMovement();
    }

    public void HandlePlayerRotation()
    {
        float horizontalInput = Input.GetAxis("Horizontal");
        _rigidbody.MoveRotation(Quaternion.Euler(0, horizontalInput, 0) * _rigidbody.rotation);
    }

    public void HandlePlayerMovement()
    {
        float verticalInput = Input.GetAxis("Vertical");
        _rigidbody.AddForce(_playerTransform.forward * verticalInput * _moveSpeed, ForceMode.VelocityChange);
    }

    public void HandleCameraRotation()
    {
        float mouseY = Input.GetAxis("Mouse Y") * _mouseSensitivity;
        _cameraRotationX += mouseY;
        _cameraRotationX = Mathf.Clamp(_cameraRotationX, -_clampRotationDegree, _clampRotationDegree);
        transform.localRotation = Quaternion.Euler(_cameraRotationX, 0, 0);
    }
}

Indeed! It miraclously fixed the issue with corners, but! The player has inertia now, and in addition I can fix the issue with corner pushback by some manipulations with physical properties Linear Dumping and Angular Dumping without use of any AddForce() method, so it does not work in general… AddForce approach creates more problems.

To place methods of rotation and movement in different Update-methods was a good idea! Now it works less choppy, I wonder why it is so?

You can decrease the mass and increase the drag to reduce the inertia.

The rotation is smoother from within Update because it’s updated at the monitor’s refresh rate while FixedUpdate is updated at 50hz. So despite only 50hz rigidbody movement is made smooth by using interpolation but this only applies to movement from AddForce or AddTorque on dynamic rigidbodies, or MovePosition and MoveRotation on kinematic rigidbodies.

The problem I can solve this without any AddForce() which adds drag motion by inertia by just increasing Dumping parameters. But what I want is to solve the corner problem without blatantly adjusting physical parameters.

Mr. Zulo, do you know how to use kinematic stuff? I mean when I tried to use, its only react on “Is Trigger” colliders, but what I want:

  1. Detect “Is Trigger” collider. The Narrator object playing sound when player reaches particular area.

  2. Detect colliders — I used it to block motion of player through walls.

Maybe I should block moves of the player through walls by something different than simply stashed box colliders?

If you don’t want inertia at all then you can remove any velocity when there’s no input or you could switch to using a kinematic controller and move it around using MovePosition, but then you’ll have to manage the collisions which is probably going to be too difficult for you. Another solution is to switch back to using the CharacterController.

The kinematic program does not detect common colliders, only “Is Trigger” type.

CharacterController did not design for fly motion, as for me it is for walking like stuff. I tried it, couple bloody days ago, when I started to solve this problem, and something goes wrong with it…

I think I see the light in the end of the tunnel.


It seems that the problem of drag-force which parallel to the wall appears because of the two green and two yellow lines. Green line is raycast to detect collider, the yellow one is adjusted movement to prevent player fly inside the wall.

using UnityEngine;
using UnityEngine.EventSystems;

// Program to control simple camera with collider on desired height, with help of Mr. Gemini (AI Google)
public class CombinedCameraController : MonoBehaviour
{
    // FIELDS

    [SerializeField]
    float _mouseSensitivity = 2.0f;
    [SerializeField]
    float _rotationSpeed = 100.0f;
    [SerializeField]
    float _smoothTime = 0.5f;
    [SerializeField]
    float _moveSpeed = 3.0f;
    [SerializeField]
    float _raycastOffset = 0.2f;
    [SerializeField]
    float _moveDistanceOffset = 0.3f;
    [SerializeField]
    float _clampRotationDegree = 80f;
    [SerializeField]
    float _minMoveMagnitude = 0.001f;
    [SerializeField]
    float _cameraRotationX = 0f;
    [SerializeField]
    float _playerRotationY = 0f;
    [SerializeField]
    float _triggerColliderDetectionRadius = 0.5f;
    [SerializeField]
    float _collisionCheckDistance = 0.1f;
    [SerializeField]
    float _pushBackPreventionDistance = 0.01f;
    [SerializeField]
    float _debugRayLength = 1.0f;

    RaycastHit _debugHit;
    Vector3 _debugAdjustedMoveDirection;
    Vector3 _debugMoveDirection;
    Vector3 _debugRaycastOrigin;
    bool _isMoving;
    bool _drawDebugRays;

    Vector3 _lastPosition;
    Rigidbody _rigidbody;
    Transform _playerTransform;
    Quaternion _cameraInitialLocalRotation;

    // METHODS

    // This method calls before the first frame

    void Start()
    {
        _playerTransform = transform.parent;

        _rigidbody = GetComponentInParent<Rigidbody>();

        if (_playerTransform == null)
        {
            Debug.LogError("Parent Transform not found!");

            enabled = false;

            return;
        }

        if (_rigidbody == null)
        {
            Debug.Log("Initialization ERROR! Parent Rigidbody component cannot be found.");

            enabled = false;

            return;
        }

        _cameraInitialLocalRotation = transform.localRotation;

        _rigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
        //_rigidbody.isKinematic = true;
        _rigidbody.useGravity = false;
        _rigidbody.freezeRotation = true;

        _lastPosition = _rigidbody.position;

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    // This method calls per frame

    void FixedUpdate()
    {
        HandlePlayerMovement();
    }

    void Update()
    {
        HandlePlayerRotation();
        HandleCameraRotation();

        //OnDrawGizmosSelected();

#if UNITY_EDITOR
        if (_isMoving)
        {
            if (_drawDebugRays)
            {
                Debug.DrawRay(_debugRaycastOrigin, _debugMoveDirection * 2f, Color.red, Time.deltaTime);
                Debug.DrawRay(_debugRaycastOrigin, _debugMoveDirection.normalized * _debugHit.distance, Color.green, Time.deltaTime);
                Debug.DrawRay(_rigidbody.position, _debugAdjustedMoveDirection, Color.yellow, Time.deltaTime);
                Debug.Log($"HIT DISTANCE: {_debugHit.distance}\nADJUSTED MOVE: {_debugAdjustedMoveDirection}");
            }
            else
            {
                Debug.DrawRay(_debugRaycastOrigin, _debugMoveDirection * 2f, Color.blue, Time.deltaTime);
            }
        }
        #endif
    }

    // Only left and right

    public void HandlePlayerRotation()
    {
        // Player Rotation (Y-axis Rotation - A/D keys)
        float horizontalInput = Input.GetAxis("Horizontal");
        _playerRotationY += horizontalInput * _rotationSpeed * Time.deltaTime;

        // Smooth player rotation using Quaternion.Slerp (Spherical-linear interpolation)
        Quaternion targetPlayerRotation = Quaternion.Euler(0f, _playerRotationY, 0f);
        _playerTransform.localRotation = Quaternion.Slerp(_playerTransform.localRotation, targetPlayerRotation, _smoothTime);
    }

    // Only back and forward

    public void HandlePlayerMovement()
    {
        float movePerFrame = _moveSpeed * Time.fixedDeltaTime;

        float verticalInput = Input.GetAxis("Vertical");

        Vector3 moveDirection = _playerTransform.forward * movePerFrame * verticalInput;

        _isMoving = moveDirection.magnitude > _minMoveMagnitude;

        if (_isMoving)
        {
            Vector3 adjustedDirection;

            if (CheckCollision(moveDirection, out adjustedDirection))
            {
                _rigidbody.MovePosition(_rigidbody.position + adjustedDirection);
            }
            else
            {
                _rigidbody.MovePosition(_rigidbody.position + moveDirection);
            }
        }
        else
        {
            _drawDebugRays = false;
        }
    }

    // Only up and down

    public void HandleCameraRotation()
    {
        // Mouse Look (Camera X-axis Rotation)
        float mouseY = Input.GetAxis("Mouse Y") * _mouseSensitivity;
        _cameraRotationX += mouseY;
        _cameraRotationX = Mathf.Clamp(_cameraRotationX, -_clampRotationDegree, _clampRotationDegree);

        // Smooth camera rotation using Quaternion.Slerp (THIS IS THE KEY by Mr. Gemini)
        Quaternion targetCameraRotation = _cameraInitialLocalRotation * Quaternion.Euler(_cameraRotationX, 0f, 0f);
        transform.localRotation = Quaternion.Slerp(transform.localRotation, targetCameraRotation, _smoothTime);
    }

    public bool CheckCollision(Vector3 moveDirection, out Vector3 adjustedMoveDirection)
    {    
        adjustedMoveDirection = moveDirection;

        Vector3 raycastOrigin = _playerTransform.position + moveDirection.normalized * _collisionCheckDistance;

        _debugMoveDirection = moveDirection;
        _debugRaycastOrigin = raycastOrigin;

        float raycastDistance = moveDirection.magnitude + _collisionCheckDistance;

        bool isCollider = Physics.Raycast(raycastOrigin, moveDirection, out RaycastHit hit, moveDirection.magnitude + _raycastOffset);

        if ((hit.collider != null) && !hit.collider.isTrigger) // Only adjust movement if hit non-trigger collider
        {
            float moveDistance = hit.distance - _collisionCheckDistance - _pushBackPreventionDistance;

            if (moveDistance > 0f)
            {
                adjustedMoveDirection = moveDirection.normalized * moveDistance;

                // Prvent vertical motion
                adjustedMoveDirection.y = 0;

                _debugHit = hit;
                _debugAdjustedMoveDirection = adjustedMoveDirection;
                

                _drawDebugRays = true;

                return true;
            }
            else
            {
                _drawDebugRays = false;
            }
        }

        return false;
    }

    public Collider[] HandleTriggerDetection()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position, _triggerColliderDetectionRadius);

        foreach (Collider collider in hitColliders)
        {
            if (collider.isTrigger)
            {
                Debug.Log("Trigger entered: " + collider.name);
            }
        }

        return hitColliders;
    }

    public void PreventPushback()
    {
        _rigidbody.MovePosition(_lastPosition); 
    }

    //private void OnDrawGizmosSelected()
    //{
    //    if (_isColliding)
    //    {
    //        Gizmos.color = Color.red;
    //        Gizmos.DrawLine(_debugRaycastOrigin, _debugMoveDirection);

    //        Gizmos.color = Color.green;
    //        Gizmos.DrawLine(_debugRaycastOrigin, _debugMoveDirection.normalized * _debugHit.distance);

    //        Gizmos.color = Color.yellow;
    //        Gizmos.DrawLine(_rigidbody.position, _debugAdjustedMoveDirection);
    //    }
    //}
}

Maybe somebody will see it useful — the script I created with help of AI Gemini to follow the player object during debugging MS Visual Studio in the scene view. It should be placed into Assets —> Editor

using UnityEngine;
using UnityEditor;

[InitializeOnLoad]
public class SceneViewFollow : MonoBehaviour
{
    [SerializeField]
    static Transform _target;
    [SerializeField]
    static Vector3 _offset = new Vector3(0, 2, 0);

    static SceneViewFollow()
    {
        SceneView.duringSceneGui += OnSceneGui;
    }

    static void OnSceneGui(SceneView sceneView)
    {
        if ((Selection.activeTransform != null) && Selection.activeTransform.CompareTag("Player"))
        {
            _target = Selection.activeTransform;
        }
        else
        {
            _target = null;
        }

        if (_target != null)
        {
            sceneView.FrameSelected();

            //Quaternion targetRotation = _target.rotation;

            //Vector3 targetPosition = _target.position + (targetRotation * _offset);

            //sceneView.LookAt(targetPosition, targetRotation, 5);
        }
    }
}

How to fix ‘drag force’ if angle different from 90 degrees in collision?

Also I need to organize the code OMG… I mean need one class for debug stuff, of few… etc cetra, can somebody make some advice? To this process not take another few days… :neutral_face:

Any idea why I raycast made in opposite direction to movement?

using UnityEngine;
using System.Collections.Generic;

// Program to control simple camera with collider on desired height, with help of Mr. Gemini (AI Google)
public class CombinedCameraController : MonoBehaviour
{
    // FIELDS

    [SerializeField]
    float _mouseSensitivity = 2.0f;
    [SerializeField]
    float _rotationSpeed = 100.0f;
    [SerializeField]
    float _smoothTime = 0.5f;
    [SerializeField]
    float _moveSpeed = 3.0f;
    [SerializeField]
    float _raycastOffset = 0.2f;
    [SerializeField]
    float _moveDistanceOffset = 0.3f;
    [SerializeField]
    float _clampRotationDegree = 80f;
    [SerializeField]
    float _minMoveMagnitude = 0.0001f;
    [SerializeField]
    float _cameraRotationX = 0f;
    [SerializeField]
    float _playerRotationY = 0f;
    [SerializeField]
    float _triggerColliderDetectionRadius = 0.5f;
    [SerializeField]
    float _collisionCheckDistance = 0.01f;
    [SerializeField]
    float _pushBackPreventionDistance = 0.001f;
    [SerializeField]
    float _debugRayLength = 1.0f;

    bool _isMoving;

    Vector3 _lastPosition;
    Rigidbody _rigidbody;
    Transform _playerTransform;
    Quaternion _cameraInitialLocalRotation;

    // METHODS

    // This method calls before the first frame

    void Start()
    {
        _playerTransform = transform.parent;

        _rigidbody = GetComponentInParent<Rigidbody>();

        if (_playerTransform == null)
        {
            Debug.LogError("Parent Transform not found!");

            enabled = false;

            return;
        }

        if (_rigidbody == null)
        {
            Debug.Log("Initialization ERROR! Parent Rigidbody component cannot be found.");

            enabled = false;

            return;
        }

        _cameraInitialLocalRotation = transform.localRotation;

        _rigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
        //_rigidbody.isKinematic = true;
        _rigidbody.useGravity = false;
        _rigidbody.freezeRotation = true;

        _lastPosition = _rigidbody.position;

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    // This method calls per frame

    void FixedUpdate()
    {
        HandlePlayerMovement();
    }

    void Update()
    {
        HandlePlayerRotation();
        HandleCameraRotation();
    }

    // Restricted to only left and right

    public void HandlePlayerRotation()
    {
        // Player Rotation (Y-axis Rotation - A/D keys)
        float horizontalInput = Input.GetAxis("Horizontal");
        _playerRotationY += horizontalInput * _rotationSpeed * Time.deltaTime;

        // Smooth player rotation using Quaternion.Slerp (Spherical-linear interpolation)
        Quaternion targetPlayerRotation = Quaternion.Euler(0f, _playerRotationY, 0f);
        _playerTransform.localRotation = Quaternion.Slerp(_playerTransform.localRotation, targetPlayerRotation, _smoothTime);
    }

    // Only back and forward

    public void HandlePlayerMovement()
    {
        float movePerFrame = _moveSpeed * Time.fixedDeltaTime;

        float verticalInput = Input.GetAxis("Vertical");

        //Debug.Log("verticalInput: " + verticalInput);

        Vector3 moveDirection = _playerTransform.forward * movePerFrame * verticalInput;

        //Debug.Log("_playerTransform.forward: " + _playerTransform.forward);

        _isMoving = moveDirection.magnitude > _minMoveMagnitude;

        if (_isMoving)
        {
            Vector3 adjustedDirection;

            if (CheckCollision(moveDirection, out adjustedDirection))
            {
                //Debug.Log("modeDirection: " + moveDirection);

                Debug.DrawRay(_rigidbody.position, adjustedDirection, Color.red, 0.1f);
                
                _rigidbody.MovePosition(_rigidbody.position + adjustedDirection);
            }
            else
            {
                //Debug.Log("_rigidbody.position" + _rigidbody.position);

                //Debug.Log("moveDirection: " + moveDirection);

                Debug.DrawRay(_rigidbody.position, moveDirection, Color.green, 0.1f);

                _rigidbody.MovePosition(_rigidbody.position + moveDirection);
            }
        }
    }

    // Only up and down

    public void HandleCameraRotation()
    {
        // Mouse Look (Camera X-axis Rotation)
        float mouseY = Input.GetAxis("Mouse Y") * _mouseSensitivity;
        _cameraRotationX += mouseY;
        _cameraRotationX = Mathf.Clamp(_cameraRotationX, -_clampRotationDegree, _clampRotationDegree);

        // For smooth camera rotation using Quaternion.Slerp (THIS IS THE KEY by Mr. Gemini)
        Quaternion targetCameraRotation = _cameraInitialLocalRotation * Quaternion.Euler(_cameraRotationX, 0f, 0f);
        transform.localRotation = Quaternion.Slerp(transform.localRotation, targetCameraRotation, _smoothTime);
    }

    public bool CheckCollision(Vector3 moveDirection, out Vector3 projectedMove)
    {
        projectedMove = moveDirection;

        //Debug.Log("moveDirection: " + moveDirection);
        
        float raycastDistance = moveDirection.magnitude + _collisionCheckDistance;
        Vector3 raycastOrigin = _playerTransform.position + moveDirection.normalized * _collisionCheckDistance;

        RaycastHit[] allHits = Physics.RaycastAll(raycastOrigin, moveDirection, raycastDistance);

        List<RaycastHit> validHits = new List<RaycastHit>();
        
        foreach (RaycastHit currentHit in allHits)
        {
            if ((currentHit.collider != null) && !currentHit.collider.isTrigger)
            {
                validHits.Add(currentHit);
            }
        }

        if (validHits.Count > 0)
        {
            foreach (RaycastHit hit in validHits)
            {
                float moveDistance = hit.distance - _collisionCheckDistance - _pushBackPreventionDistance;

                if (moveDistance <= _minMoveMagnitude) continue;

                projectedMove = moveDirection.normalized * moveDistance;

                //projectedMove = Vector3.Project(moveDirection, hit.normal);
            }

            return true;
        }

        return false;
    }

    public Collider[] HandleTriggerDetection()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position, _triggerColliderDetectionRadius);

        foreach (Collider collider in hitColliders)
        {
            if (collider.isTrigger)
            {
                Debug.Log("Trigger entered: " + collider.name);
            }
        }

        return hitColliders;
    }
}

Maybe you should look into Cinemachine. No need to reinvent the wheel for camera work when Unity has a ready-to-go solution available.

For what purpose? To follow player in Scene view when I use the Game Mode (for that purpose I make some script with help of Gemini and it took a long time)? I am curious about Cinemachine, but I never used it, okay I will look, but it better you can describe it in few words, not just say “maybe you should use that”, because I am not experienced Unity human Mr. Spiney.

I have found the source of problem Vector3 moveDirection, somehow Debug.DrawRay() makes it mirrored…

If only there were a way, on the internet, to look up things we don’t know about.

Sarcasm aside it’s an out of the box camera solution: Cinemachine

It seems it only for Game Mode?

Are you not trying to make a camera controller for runtime?

I already made it with help of AI Gemini for Editor Window. The Cinemachine is not a solution, at least I do not know how to use in this case.

AHA! I have found the issue of back-cast of rays! The second parameter of Debug.DrawRay() method is need for normalized vector! Because it multiplies the vector on some other thing, maybe on matrix, I did not get it… And if the vector is not normalized by coordinate basis — somehow it makes the result to cast into the opposite direction.

I have found the true reason of why backward cast exist — it is because the casting ray divided on the very small portion same as vertical input from “W” key button pressed. And then vector of motion and the vector of ray build step-by-step, frame, by, frame, but due to we have some time of life of raycast, it exist longer after the origin point of cast displaced toward movement direction — and that creates effect of backward cast.