Need help to refactor the logic

I have the code which fixed the cursed knockback issue in corner with colliders (I am doing Unity Essential tutorial) and I spent a lot of time to make and with help of AI Gemini, but there is a lot of issue… First the class is too larger and too wide spread the purpose. Second I got ‘null reference exception’ after the first start and stop of Game Mode on line 260 foreach (Vector3 direction in _directions), and many other problems, please help to refactor.

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;
    [SerializeField]
    float _raycastDuration = 0.1f;
    [SerializeField]
    float _castColliderCoef = 100f;
    [SerializeField]
    float maxRaycastDistance = 5.0f;
    [SerializeField]
    int _stepViyalo = 15;

    bool _isMoving;
        
    Rigidbody _rigidbody;
    Transform _playerTransform;
    Quaternion _cameraInitialLocalRotation;

    Vector3[] _directions;
    Vector3 _lastMoveDirection;
    Vector3 _lastRaycastOrigin;
    RaycastHit _lastHit;
    bool _hasHit;

    // 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;

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

    // This method calls per frame

    void FixedUpdate()
    {
        HandlePlayerMovement();

        PrepareDebugRays();
    }

    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");
                
        Vector3 moveDirection = _playerTransform.forward * movePerFrame * verticalInput;

        _lastMoveDirection = moveDirection;

        _lastRaycastOrigin = _playerTransform.position + _lastMoveDirection.normalized * _collisionCheckDistance;

        _isMoving = moveDirection.magnitude > _minMoveMagnitude;

        if (_isMoving)
        {
            Vector3 adjustedDirection;

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

                //Debug.DrawRay(_playerTransform.position, moveDirection, Color.green, 0.1f);
            }
        }
    }

    // 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;

        Vector3[] raycastDirections = new Vector3[]
        {
            //moveDirection,
            Quaternion.Euler(0, 15, 0) * moveDirection,
            Quaternion.Euler(0, -15, 0) * moveDirection,
            Quaternion.Euler(0, 45, 0) * moveDirection,
            Quaternion.Euler(0, -45, 0) * moveDirection,
            Quaternion.Euler(0, 90, 0) * moveDirection,
            Quaternion.Euler(0, -90, 0) * moveDirection
        };
                
        float raycastDistance = moveDirection.magnitude + _collisionCheckDistance;

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

        List<RaycastHit> validHits = new List<RaycastHit>();

        foreach (Vector3 direction in raycastDirections)
        {
            RaycastHit[] allHits = Physics.RaycastAll(raycastOrigin, direction, raycastDistance);

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

        if (validHits.Count > 0)
        {
            Vector3 combinedNormal = Vector3.zero;
            
            float closestHitDistance = validHits[0].distance;

            foreach (RaycastHit hit in validHits)
            {
                float weight = 1.0f - (hit.distance / maxRaycastDistance);
                
                combinedNormal += hit.normal * weight;

                if (hit.distance < closestHitDistance)
                {
                    closestHitDistance = hit.distance;
                }

                float moveDistance = closestHitDistance - _collisionCheckDistance - _pushBackPreventionDistance;

                if (moveDistance <= _minMoveMagnitude) continue;               
            }

            combinedNormal.Normalize();

            projectedMove = Vector3.ProjectOnPlane(projectedMove, combinedNormal);

            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 PrepareDebugRays()
    {
        _directions = CreateArrayOfDirectionsAroundYAxis(_stepViyalo, _lastMoveDirection);
    }

    public void OnDrawGizmosSelected()
    {
        foreach (Vector3 direction in _directions)
        {
            _hasHit = Physics.Raycast(_lastRaycastOrigin, direction, out _lastHit, direction.magnitude + _collisionCheckDistance);

            if (_hasHit)
            {
                Gizmos.color = Color.red;
                Gizmos.DrawRay(_lastRaycastOrigin, direction.normalized * _lastHit.distance);

                Gizmos.color = Color.blue;
                Gizmos.DrawRay(_lastHit.point, _lastHit.normal);
            }
            else if (_lastMoveDirection.magnitude > _minMoveMagnitude)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawRay(_playerTransform.position, direction.normalized * _moveSpeed);
            }
        }
    }

    public Vector3[] CreateArrayOfDirectionsAroundYAxis(int stepGrad, Vector3 direction)
    {
        int length = 360 / stepGrad;

        int currentDirection = stepGrad;

        float yRotation;

        Vector3[] directions = new Vector3[length];

        for (int i = 0; i < (directions.Length - 1); i++)
        {
            yRotation = currentDirection + (stepGrad * i);

            directions[i] = Quaternion.Euler(0, yRotation, 0) * direction;
            directions[i + 1] = Quaternion.Euler(0, -yRotation, 0) * direction;

            currentDirection += stepGrad;
        }

        return directions;
    }
}
public void OnDrawGizmosSelected()
{
   if (_direcitons == null)
       _directions = new Vector3[0]; // initialize as empty

    foreach (Vector3 direction in _directions)
    {
        _hasHit = Physics.Raycast(_lastRaycastOrigin, direction, out _lastHit, direction.magnitude + _collisionCheckDistance);

        if (_hasHit)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawRay(_lastRaycastOrigin, direction.normalized * _lastHit.distance);

            Gizmos.color = Color.blue;
            Gizmos.DrawRay(_lastHit.point, _lastHit.normal);
        }
        else if (_lastMoveDirection.magnitude > _minMoveMagnitude)
        {
            Gizmos.color = Color.green;
            Gizmos.DrawRay(_playerTransform.position, direction.normalized * _moveSpeed);
        }
    }
}

When _directions hasn’t been assigned yet (before the first FixedUpdate()), initialize with a zero-length array.

So, do you say Mr. Lo-renzo, that Vector3[] CreateArrayOfDirectionsAroundYAxis(int stepGrad, Vector3 direction) called after void OnDrawGizmosSelected() but why so? I mean _directions initialize in FixedUpdate() via PrepareDebugRays() method, so it means that FixedUpdate() CALLED AFTER OnDrawGizmosSelected()… O_o I did not get it…

I mean the order of execution is clearly outlined in the docs: Unity - Manual: Order of execution for event functions

Though FixedUpdate is special in that in runs as many times as it needs to before Update, for the amount of times the fixed time-step has passed. I’d wager this means on the first frame or so it will run after OnDrawGizmosSelected.

But all this is just a distraction from the real problem… why is this array being allocated every fixed update? It should just be allocated once in Awake. Then job done.

You really shouldn’t be relying on AI to write this code for you. You won’t learn anything, and you will just have bad code on your hands. Learn to code properly.

And FWIW, if the array is null, just guard OnDrawGizmosSelected to return early:

public void OnDrawGizmosSelected()
{
	if (_directions == null)
	{
		return;
	}
	
	// draw stuff
}

I have read the article for a while, hard to understand, but thank you Mr. Spiney.

No of course, the array _directions exactly should update every time because, because the direction, the argument of the parameter of this method updates every time, it is pretty obviously, it is the direction of the movement of the player object, and the directions of the raycast calculated according this very direction.
You probably did not read the code Mr. Spiney, it is actually pretty hard to read, this is one of the reason I should refactor it.

About AI — I cannot write a code without it, because I do not know Unity programming interface, and actually in the tutorial which I follow, I do not need to do it, it is an addition. In the same time AI Gemini is fine, and I ask a lot of question about the code there.

My first attempt to refactor.

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 _moveDistanceOffset = 0.3f;
    [SerializeField] float _clampRotationDegree = 80f;
    [SerializeField] float _minMoveMagnitude = 0.0001f;
    [SerializeField] float _cameraRotationX = 0f;
    [SerializeField] float _playerRotationY = 0f;
    [SerializeField] float _collisionCheckDistance = 0.01f;
    [SerializeField] float _pushBackPreventionDistance = 0.001f;
    [SerializeField] float _raycastDuration = 0.1f;
    [SerializeField] float _castColliderCoef = 100f;
    [SerializeField] float _maxRaycastDistance = 3.0f;
    [SerializeField] int _stepViyalo = 15;

    bool _isMoving;
        
    Rigidbody _rigidbody;
    Transform _playerTransform;
    Quaternion _cameraInitialLocalRotation;

    Vector3[] _directions;
    Vector3 _lastMoveDirection;
    Vector3 _lastRaycastOrigin;
    RaycastHit _lastHit;
    bool _hasHit;

    // 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.useGravity = false;
        _rigidbody.freezeRotation = true;

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

    void FixedUpdate()
    {
        HandlePlayerMovement();

        PrepareDebugRays();
    }

    // This method calls per frame

    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");
                
        Vector3 moveDirection = _playerTransform.forward * movePerFrame * verticalInput;

        _lastMoveDirection = moveDirection;

        _lastRaycastOrigin = _playerTransform.position + _lastMoveDirection.normalized * _collisionCheckDistance;

        _isMoving = moveDirection.magnitude > _minMoveMagnitude;

        if (_isMoving)
        {
            Vector3 adjustedDirection;

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

    // 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;
                                
        float raycastDistance = moveDirection.magnitude + _collisionCheckDistance;

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

        List<RaycastHit> validHits = new List<RaycastHit>();
                
        RaycastHit[] allHits = Physics.RaycastAll(raycastOrigin, moveDirection, raycastDistance);

        foreach (RaycastHit currentHit in allHits)
        {
            if ((currentHit.collider != null) && !currentHit.collider.isTrigger)
            {
                validHits.Add(currentHit);
            }
        }
        
        if (validHits.Count > 0)
        {
            Vector3 combinedNormal = Vector3.zero;
            
            float closestHitDistance = validHits[0].distance;

            foreach (RaycastHit hit in validHits)
            {
                float weight = 1.0f - (hit.distance / _maxRaycastDistance);
                
                combinedNormal += hit.normal * weight;

                if (hit.distance < closestHitDistance)
                {
                    closestHitDistance = hit.distance;
                }

                float moveDistance = closestHitDistance - _collisionCheckDistance - _pushBackPreventionDistance;

                if (moveDistance <= _minMoveMagnitude) continue;               
            }

            combinedNormal.Normalize();

            projectedMove = Vector3.ProjectOnPlane(projectedMove, combinedNormal);

            return true;
        }

        return false;
    }

    public void PrepareDebugRays()
    {
        _directions = CreateArrayOfDirectionsAroundYAxis(_stepViyalo, _lastMoveDirection);
    }

    public void OnDrawGizmosSelected()
    {
        foreach (Vector3 direction in _directions)
        {
            _hasHit = Physics.Raycast(_lastRaycastOrigin, direction, out _lastHit, direction.magnitude + _collisionCheckDistance);

            if (_hasHit)
            {
                Gizmos.color = Color.red;
                Gizmos.DrawRay(_lastRaycastOrigin, direction.normalized * _lastHit.distance);

                Gizmos.color = Color.blue;
                Gizmos.DrawRay(_lastHit.point, _lastHit.normal);
            }
            else if (_lastMoveDirection.magnitude > _minMoveMagnitude)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawRay(_playerTransform.position, direction.normalized * _moveSpeed);
            }
        }
    }

    public Vector3[] CreateArrayOfDirectionsAroundYAxis(int stepGrad, Vector3 direction)
    {
        int length = 360 / stepGrad;

        int currentDirection = stepGrad;

        float yRotation;

        Vector3[] directions = new Vector3[length];

        for (int i = 0; i < (directions.Length - 1); i++)
        {
            yRotation = currentDirection + (stepGrad * i);

            directions[i] = Quaternion.Euler(0, yRotation, 0) * direction;
            directions[i + 1] = Quaternion.Euler(0, -yRotation, 0) * direction;

            currentDirection += stepGrad;
        }

        return directions;
    }
}

This monstroucity becomes more readable, but there are still things to fix. I think the next time I will try to use Test Driven Development instead.

Then why even use said method to allocate an array? If that method is calculating the directions, then why not use said method to do the actual drawing instead? There’s no need to allocate an array (especially not every fixed update) just to draw some gizmos.

Because to draw debug-rays we use event which described in your provided and somehow hidden method OnDrawGizmosSelected() in MonoBehaviour class which is called every frame when specific object selected. I used also class Debug, but with low success, these OnDraw… method is better work, at least easy to implement the logic of debug rays on their basis.

Just call the method in OnDrawGizmosSelected instead then.

Mr. Spiney, are you lost the conversation? I exactly called the very method. In the previous post I explained it.

Christ sake. I mean rewrite and rename CreateArrayOfDirectionsAroundYAxis to instead draw the rays. Then call that method in OnDrawGizmosSelected.

Or just do the calculations in OnDrawGizmosSelected instead. Use some imagination.

Why? The method OnDraw…something is exactly designed for drawing the stuff like gizmos, why I should to move logic of drawing these very gizmos into other methods? I did not get it, maybe I miss something?

P. S. It is not even a method but the method which calls reflection of the method, or someting, it is hard to understand that part of API Unity for me. It uses reflexion somehow.

Yes I know how Unity’s magic methods work.

And calling method’s from other methods is… Just a normal programming thing to do.

For someone who is asking for help with programming, and doesn’t understand it very well, why do you keep resisting any advice from the more experienced here?

I think I saw your posts in my previous topic, and I do not remember any good advice from you, Mr. Spiney. But instead I witness a lot of mistakes from you, where after you delete your posts…

If you do know how reflection works in Unity (you cannot find for an example methods OnDraw… in MonoBehaviour class, but they used as they were in this very class with help reflection), maybe you can explain it?

Again, I see no any good in move logic of exact drawing of gizmos from the method which is called OnDrawGizmos… It is simply has no any purpose and only make code more complex, but I want to make normal code in the future which even a single developer as me can work with.

Reflection is a C# thing, not a Unity specific thing. That said, Unity doesn’t use reflection to analyse the type, it analyses its respective MonoScript asset for the messages, caches any of the messages it has, then calls them when required, saving lots of overhead.

I would link to the blog post were Unity go into detail about this, but it seems to be missing after Unity screwed up the blogs.

You can see every Unity message a given type has in the docs: Unity - Scripting API: MonoBehaviour

Because you may have to expand or add different things you want to draw. Rather than lump these all onto one method, you can break it down into multiple methods.

private void OnDrawGizmosSelected()
{
	DrawSomeGizmos();
	DrawSomeOtherGizmos();
}

Which is exactly what you’re already doing in your Update message, by the way.

You not understanding how to use a package isn’t bad advice on my part. And where did I delete my posts? (aside from just above where I hit the wrong button on mobile).

Thank, I see the ‘messages’ there, but still it looks pretty complicated, I understand the purpose of it — to reduce the size of the class, but still.

I understand your idea, but that Gizmos class’s methods should be used in OnDraw… methods directly at least according to Unity API suggestions: Unity - Scripting API: Gizmos

My pardon, I mistaken you with zulo3d username. But your idea to use Cinemachine and other stuff is also not good, when I asked to why raycast in opposite direction, you said to use Cinemachine: Camera controller script with collider - #19 by Frostysh

My next step in refactoring the code, I reduced it to 250 lines, also made some renaming

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 _maxCameraRotationAngle = 80f;
    [SerializeField] float _minMoveMagnitude = 0.0001f;
    [SerializeField] float _cameraRotationX = 0f;
    [SerializeField] float _playerRotationY = 0f;
    [SerializeField] float _collisionCheckDistance = 0.01f;
    [SerializeField] float _pushBackPreventionDistance = 0.001f;
    [SerializeField] float _maxRaycastDistance = 3.0f;
    [SerializeField] int _debugRaySteps = 15;

    bool _isMoving;
        
    Rigidbody _playerRigidbody;
    Transform _playerTransform;
    Quaternion _cameraInitialLocalRotation;

    // For analyze purpose

    Vector3[] _directions;
    Vector3 _lastMoveDirection;
    Vector3 _lastRaycastOrigin;
    RaycastHit _lastHit;
    bool _hasHit;

    // METHODS

    // This method calls before the first frame

    void Start()
    {
        _playerTransform = transform.parent;

        _playerRigidbody = GetComponentInParent<Rigidbody>();

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

            enabled = false;

            return;
        }

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

            enabled = false;

            return;
        }

        _cameraInitialLocalRotation = transform.localRotation;

        _playerRigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
        _playerRigidbody.useGravity = false;
        _playerRigidbody.freezeRotation = true;

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

    void FixedUpdate()
    {
        HandlePlayerMovement();

        PrepareDebugRays();
    }

    // This method calls per frame

    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");
                
        Vector3 moveDirection = _playerTransform.forward * movePerFrame * verticalInput;

        _lastMoveDirection = moveDirection;

        _lastRaycastOrigin = _playerTransform.position + _lastMoveDirection.normalized * _collisionCheckDistance;

        _isMoving = moveDirection.magnitude > _minMoveMagnitude;

        if (_isMoving)
        {
            Vector3 adjustedDirection;

            if (CheckCollision(moveDirection, out adjustedDirection))
            {               
                _playerRigidbody.MovePosition(_playerRigidbody.position + adjustedDirection);
            }
            else
            {                
                _playerRigidbody.MovePosition(_playerRigidbody.position + adjustedDirection);
            }
        }
    }

    // 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, -_maxCameraRotationAngle, _maxCameraRotationAngle);

        // 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;
                                
        float raycastDistance = moveDirection.magnitude + _collisionCheckDistance;

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

        List<RaycastHit> validHits = new List<RaycastHit>();
                
        RaycastHit[] allHits = Physics.RaycastAll(raycastOrigin, moveDirection, raycastDistance);

        foreach (RaycastHit currentHit in allHits)
        {
            if ((currentHit.collider != null) && !currentHit.collider.isTrigger)
            {
                validHits.Add(currentHit);
            }
        }
        
        if (validHits.Count > 0)
        {
            Vector3 combinedNormal = Vector3.zero;
            
            float closestHitDistance = validHits[0].distance;

            foreach (RaycastHit hit in validHits)
            {
                float weight = 1.0f - (hit.distance / _maxRaycastDistance);
                
                combinedNormal += hit.normal * weight;

                if (hit.distance < closestHitDistance)
                {
                    closestHitDistance = hit.distance;
                }

                float moveDistance = closestHitDistance - _collisionCheckDistance - _pushBackPreventionDistance;

                if (moveDistance <= _minMoveMagnitude) continue;               
            }

            combinedNormal.Normalize();

            projectedMove = Vector3.ProjectOnPlane(projectedMove, combinedNormal);

            return true;
        }

        return false;
    }

    public void PrepareDebugRays()
    {
        _directions = CreateArrayOfDirectionsAroundYAxis(_debugRaySteps, _lastMoveDirection);
    }

    public void OnDrawGizmosSelected()
    {
        foreach (Vector3 direction in _directions)
        {
            _hasHit = Physics.Raycast(_lastRaycastOrigin, direction, out _lastHit, direction.magnitude + _collisionCheckDistance);

            if (_hasHit)
            {
                Gizmos.color = Color.red;
                Gizmos.DrawRay(_lastRaycastOrigin, direction.normalized * _lastHit.distance);

                Gizmos.color = Color.blue;
                Gizmos.DrawRay(_lastHit.point, _lastHit.normal);
            }
            else if (_lastMoveDirection.magnitude > _minMoveMagnitude)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawRay(_playerTransform.position, direction.normalized * _moveSpeed);
            }
        }
    }

    public Vector3[] CreateArrayOfDirectionsAroundYAxis(int stepGrad, Vector3 direction)
    {
        int length = 360 / stepGrad;

        int currentDirection = stepGrad;

        float yRotation;

        Vector3[] directions = new Vector3[length];

        for (int i = 0; i < (directions.Length - 1); i++)
        {
            yRotation = currentDirection + (stepGrad * i);

            directions[i] = Quaternion.Euler(0, yRotation, 0) * direction;
            directions[i + 1] = Quaternion.Euler(0, -yRotation, 0) * direction;

            currentDirection += stepGrad;
        }

        return directions;
    }
}

It only matters that the timing of the method is called within OnDrawGizmos/Selected in the overarching play loop. Which we hook into with the OnDrawGizmos/Selected Unity message.

It’s totally fine to call another method that draws Gizmos in it. For example:

public class TestMonoBehaviour : MonoBehaviour
{
	private void OnDrawGizmos()
	{
		DrawSphereGrid(10, 10, 1);
 }

	private void DrawSphereGrid(int xMax, int yMax, float offset)
	{
		for (int x = 0; x <= xMax; x++)
		{
			for (int y = 0; y <= yMax; y++)
			{
				float xPos = x * offset;
				float yPos = y * offset;
				Vector3 position = new(xPos, 0, yPos);
				DrawWireSphere(position, 0.5f);
			}
		}
	}

	private void DrawWireSphere(Vector3 position, float radius)
	{
		Gizmos.DrawWireSphere(position, radius);
	}
}

This has no problems drawing a grid of spheres:

Christ, it was just a general comment, not a response to that specific question.

Hmm, in this case, maybe I should try your approach Mr. Spiney, it indeed looks not a bad idea to separate the logic into standalone methods, and then call it by OnDraw… method. Thank you.

Hmm, me did not understand my pardon.

Should I to create a separate type to hold debug fields?

I did suggested improvements and the program still works! Very good it is, despite the code still looks bad… I think I done for today (or not) with Unity, because it already 1:30 AM… And the stupid reality will punish me tommorow, so maybe I should play into video-game or watch something and continue after the sleep.

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 _maxCameraRotationAngle = 80f;
    [SerializeField] float _minMoveMagnitude = 0.0001f;
    [SerializeField] float _cameraRotationX = 0f;
    [SerializeField] float _playerRotationY = 0f;
    [SerializeField] float _collisionCheckDistance = 0.01f;
    [SerializeField] float _pushBackPreventionDistance = 0.001f;
    [SerializeField] float _maxRaycastDistance = 3.0f;
    [SerializeField] int _debugRaySteps = 15;

    bool _isMoving;
        
    Rigidbody _playerRigidbody;
    Transform _playerTransform;
    Quaternion _cameraInitialLocalRotation;

    // For analyze purpose

    Vector3[] _directions;
    Vector3 _lastMoveDirection;
    Vector3 _lastRaycastOrigin;
    RaycastHit _lastHit;
    bool _hasHit;

    // METHODS

    // This method calls before the first frame

    void Start()
    {
        _playerTransform = transform.parent;

        _playerRigidbody = GetComponentInParent<Rigidbody>();

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

            enabled = false;

            return;
        }

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

            enabled = false;

            return;
        }

        _cameraInitialLocalRotation = transform.localRotation;

        _playerRigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
        _playerRigidbody.useGravity = false;
        _playerRigidbody.freezeRotation = true;

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

    void FixedUpdate()
    {
        HandlePlayerMovement();

        PrepareDebugRays();
    }

    // This method calls per frame

    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");
                
        Vector3 moveDirection = _playerTransform.forward * movePerFrame * verticalInput;

        _lastMoveDirection = moveDirection;

        _lastRaycastOrigin = _playerTransform.position + _lastMoveDirection.normalized * _collisionCheckDistance;

        _isMoving = moveDirection.magnitude > _minMoveMagnitude;

        if (_isMoving)
        {
            Vector3 adjustedDirection;

            if (CheckCollision(moveDirection, out adjustedDirection))
            {               
                _playerRigidbody.MovePosition(_playerRigidbody.position + adjustedDirection);
            }
            else
            {                
                _playerRigidbody.MovePosition(_playerRigidbody.position + adjustedDirection);
            }
        }
    }

    // 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, -_maxCameraRotationAngle, _maxCameraRotationAngle);

        // 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;
                                
        float raycastDistance = moveDirection.magnitude + _collisionCheckDistance;

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

        List<RaycastHit> validHits = new List<RaycastHit>();
                
        RaycastHit[] allHits = Physics.RaycastAll(raycastOrigin, moveDirection, raycastDistance);

        foreach (RaycastHit currentHit in allHits)
        {
            if ((currentHit.collider != null) && !currentHit.collider.isTrigger)
            {
                validHits.Add(currentHit);
            }
        }
        
        if (validHits.Count > 0)
        {
            Vector3 combinedNormal = Vector3.zero;
            
            float closestHitDistance = validHits[0].distance;

            foreach (RaycastHit hit in validHits)
            {
                float weight = 1.0f - (hit.distance / _maxRaycastDistance);
                
                combinedNormal += hit.normal * weight;

                if (hit.distance < closestHitDistance)
                {
                    closestHitDistance = hit.distance;
                }

                float moveDistance = closestHitDistance - _collisionCheckDistance - _pushBackPreventionDistance;

                if (moveDistance <= _minMoveMagnitude) continue;               
            }

            combinedNormal.Normalize();

            projectedMove = Vector3.ProjectOnPlane(projectedMove, combinedNormal);

            return true;
        }

        return false;
    }

    public void PrepareDebugRays()
    {
        _directions = CreateArrayOfDirectionsAroundYAxis(_debugRaySteps, _lastMoveDirection);
    }

    public void OnDrawGizmosSelected()
    {
        DrawAnalyzeRaysAllDirections();
    }

    public Vector3[] CreateArrayOfDirectionsAroundYAxis(int stepGrad, Vector3 direction)
    {
        int length = 360 / stepGrad;

        int currentDirection = stepGrad;

        float yRotation;

        Vector3[] directions = new Vector3[length];

        for (int i = 0; i < (directions.Length - 1); i++)
        {
            yRotation = currentDirection + (stepGrad * i);

            directions[i] = Quaternion.Euler(0, yRotation, 0) * direction;
            directions[i + 1] = Quaternion.Euler(0, -yRotation, 0) * direction;

            currentDirection += stepGrad;
        }

        return directions;
    }

    public void DrawAnalyzeRaysAllDirections()
    {
        foreach (Vector3 direction in _directions)
        {
            _hasHit = Physics.Raycast(_lastRaycastOrigin, direction, out _lastHit, direction.magnitude + _collisionCheckDistance);

            if (_hasHit)
            {
                Gizmos.color = Color.red;
                Gizmos.DrawRay(_lastRaycastOrigin, direction.normalized * _lastHit.distance);

                Gizmos.color = Color.blue;
                Gizmos.DrawRay(_lastHit.point, _lastHit.normal);
            }
            else if (_lastMoveDirection.magnitude > _minMoveMagnitude)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawRay(_playerTransform.position, direction.normalized * _moveSpeed);
            }
        }
    }
}