Colliders at the edge of a camera

Colliders at the edge of a camera

Part 6 โ€“ From idea to game: A beginner's guide to creating games with Unity!

Feb 20, 2023ยท

11 min read

I want to improve the movement of my fish. In my last article I created the movement, but all the "fish" does is move back and forth. There are two specific things I know I will need sooner or later: random movement (When the fish is idle) and specific movement (When they are going for food). However, creating a method for random movement now will probably result in the GameObject going off-screen rather quickly.

So before I can improve the movement, I need to make sure that the fish know where they are allowed to go. Or, more specifically, where they are not. So I want to limit the area fish can go to the screen.

Changing the camera ๐Ÿ“น

I made some changes to "Main Camera" via the Unity editor:

Changed projection from perspective to orthographic.

Imagine you're drawing a scene with a pencil on a piece of paper. A perspective camera would be like drawing the objects in a way that makes them look like they're receding into the distance so that objects further away look smaller and objects closer up look bigger. This creates a sense of depth and space, making the scene feel more immersive and realistic.

On the other hand, an orthographic camera would be like drawing the objects as if they're all on the same plane, without any changes in size based on distance.

This becomes clear if you change it in the editor with the camera selected, as you can see what the camera captures (You might have to turn on Gizmos by clicking the far right button in the top bar of the Scene tab).

Visual representation of changing settings on the main camera in the Unity editor. Changing it from perspective to orthographic, increasing the size to 15 and changing the clipping planes to 10 and 30.

Changed the size

I upped the size to 15. Not much else to mention here. Increasing the size increases the size ๐Ÿคทโ€โ™‚๏ธ.

Changed the clipping planes

There are two values here. Near and Far. Adjusting the values shows how the box changes. For example, imagine your camera is at 0, 0, 0 and had a Near clipping plane of 10 and a Far clipping plane of 30. If you would place a game object at 0, 0, 5 (in front of the camera) it would not be shown if you press play. That's because the camera will only capture what is between the two clipping planes. So in short, this setting allows you how "far" your camera captures things.

Changed the position

Additionally, I moved the "Main Camera" to 0,0,0 (And "Fish" to 0,0,20), but that will not make a difference.

Creating colliders ๐Ÿงฑ

Now that it was visualized what is captured by the "Main Camera", it was time to create colliders, that were snug against the edge of what is visible. We can do this by hand, via the editor, and that is faster and could work for you.

However, I wanted a more flexible system. What if the screen size changes? Or what if the size of the camera changes down the line? I do not want to go back to adjust the colliders every time. So instead, I created a script (Called "CameraBorderCollider") to do it at runtime.

I will be honest, I had some ideas on how to do this, but could not get it to work completely. The code in this answer on the Unity forums by Invertex was super important to tie my loose ends together. As always, here is the full script, before I break it down for you.

using UnityEngine;

/// <summary>
/// Creates colliders for the camera that can be used for border detection.
/// </summary>
public class CameraBorderCollider : MonoBehaviour
{
    // Camera
    private Camera _mainCamera;
    private float _cameraClipDistance;
    private Vector3 _cameraPosition;
    private Vector3 _cameraClipCenter;

    // Colliders
    private GameObject _topCollider;
    private GameObject _bottomCollider;
    private GameObject _leftCollider;
    private GameObject _rightCollider;
    private GameObject _frontCollider;
    private GameObject _backCollider;

    // Misc.
    private Vector2 _screenSize;
    private const float _colliderDepth = 4.0f;

    /// <summary>
    /// This method is called on the first frame when the script is enabled. It sets up _mainCamera, _screenSize, 
    /// _cameraPosition, _cameraClipCenter and calls the CreateColliders method to create all colliders.
    /// </summary>
    void Start()
    {
        _mainCamera = GetComponent<Camera>();
        _cameraClipDistance = _mainCamera.farClipPlane - _mainCamera.nearClipPlane;
        _cameraPosition = _mainCamera.transform.position;
        _cameraClipCenter = new Vector3(_cameraPosition.x, _cameraPosition.y, _cameraPosition.z + _mainCamera.nearClipPlane + (_cameraClipDistance / 2));
        _screenSize.x = Vector2.Distance(_mainCamera.ScreenToWorldPoint(new Vector2(0, 0)), _mainCamera.ScreenToWorldPoint(new Vector2(Screen.width, 0)));
        _screenSize.y = Vector2.Distance(_mainCamera.ScreenToWorldPoint(new Vector2(0, 0)), _mainCamera.ScreenToWorldPoint(new Vector2(0, Screen.height)));
        CreateColliders();
    }

    /// <summary>
    /// Creates all 6 colliders. 
    /// </summary>
    private void CreateColliders()
    {
        CreateTopCollider();
        CreateBottomCollider();
        CreateLeftCollider();
        CreateRightCollider();
        CreateFrontCollider();
        CreateBackCollider();
    }

    /// <summary>
    /// Creates the top collider.
    /// </summary>
    private void CreateTopCollider()
    {
        _topCollider = new GameObject
        {
            name = "Top Collider"
        };
        _topCollider.gameObject.AddComponent<BoxCollider>();
        _topCollider.transform.parent = transform;
        _topCollider.transform.localScale = new Vector3(_screenSize.x, _colliderDepth, _cameraClipDistance);
        _topCollider.transform.position = new Vector3(_cameraPosition.x, _cameraPosition.y + (_screenSize.y / 2.0f) + (_topCollider.transform.localScale.y / 2.0f), _cameraClipCenter.z);
    }

    /// <summary>
    /// Creates the bottom collider.
    /// </summary>
    private void CreateBottomCollider()
    {
        _bottomCollider = new GameObject
        {
            name = "Bottom Collider"
        };
        _bottomCollider.gameObject.AddComponent<BoxCollider>();
        _bottomCollider.transform.parent = transform;
        _bottomCollider.transform.localScale = new Vector3(_screenSize.x, _colliderDepth, _cameraClipDistance);
        _bottomCollider.transform.position = new Vector3(_cameraPosition.x, _cameraPosition.y - (_screenSize.y / 2.0f) - (_bottomCollider.transform.localScale.y / 2.0f), _cameraClipCenter.z);
    }

    /// <summary>
    /// Creates the left collider.
    /// </summary>
    private void CreateLeftCollider()
    {
        _leftCollider = new GameObject
        {
            name = "LeftCollider"
        };
        _leftCollider.gameObject.AddComponent<BoxCollider>();
        _leftCollider.transform.parent = transform;
        _leftCollider.transform.localScale = new Vector3(_colliderDepth, _screenSize.y, _cameraClipDistance);
        _leftCollider.transform.position = new Vector3(_cameraPosition.x - (_screenSize.x / 2.0f) - (_leftCollider.transform.localScale.x / 2.0f), _cameraPosition.y, _cameraClipCenter.z);
    }

    /// <summary>
    /// Creates the right collider.
    /// </summary>
    private void CreateRightCollider()
    {
        _rightCollider = new GameObject
        {
            name = "Right Collider"
        };
        _rightCollider.gameObject.AddComponent<BoxCollider>();
        _rightCollider.transform.parent = transform;
        _rightCollider.transform.localScale = new Vector3(_colliderDepth, _screenSize.y, _cameraClipDistance);
        _rightCollider.transform.position = new Vector3(_cameraPosition.x + (_screenSize.x / 2.0f) + (_rightCollider.transform.localScale.x / 2.0f), _cameraPosition.y, _cameraClipCenter.z);
    }

    /// <summary>
    /// Creates the front collider.
    /// </summary>
    private void CreateFrontCollider()
    {
        _frontCollider = new GameObject
        {
            name = "Front Collider"
        };
        _frontCollider.gameObject.AddComponent<BoxCollider>();
        _frontCollider.transform.parent = transform;
        _frontCollider.transform.localScale = new Vector3(_screenSize.x, _screenSize.y, _colliderDepth);
        _frontCollider.transform.position = new Vector3(_cameraPosition.x, _cameraPosition.y, _cameraClipCenter.z + (_cameraClipDistance / 2.0f) + (_frontCollider.transform.localScale.z / 2.0f));
    }

    /// <summary>
    /// Creates the back collider.
    /// </summary>
    private void CreateBackCollider()
    {
        _backCollider = new GameObject
        {
            name = "Back Collider"
        };
        _backCollider.gameObject.AddComponent<BoxCollider>();
        _backCollider.transform.parent = transform;
        _backCollider.transform.localScale = new Vector3(_screenSize.x, _screenSize.y, _colliderDepth);
        _backCollider.transform.position = new Vector3(_cameraPosition.x, _cameraPosition.y, _cameraClipCenter.z - (_cameraClipDistance / 2.0f) - (_backCollider.transform.localScale.z / 2.0f));
    }
}

It has become quite a few rules of text, but to be fair the last half of the code is six times the same method.

Let us start at the top. I defined a lot of attributes, but there is only one I want to touch upon and that is _colliderDepth. And for two reasons. First, it is labelled as a const, and second, it has a value of 4. const Is short for "constant" and simply means we tell the code that this value will not change during runtime. 4 is important (But can be any other number) as it represents the thickness of the colliders I made.

Let me break down Start() for you. I planned to attach this script to the "Main Camera" GameObject because that makes sense since the colliders are based on what the camera captures. If you click on the "Main Camera" GameObject in the Hierarchy tab, you will see the following (Minus the script):

Screenshot of the inspector tab in Unity editor with the Main Camera GameObject selected. It shows the Transform, Camera, Audio Listener and script components attached to it.

It is important to understand what this visualises (Besides all the different settings). Remember, I selected a GameObject and in this tab, I see 4 different components (Transform, Camera, Audio Listener, Camera Border Collider). This setup is important to understand, as in the code I call GetComponent(). You can find the documentation here, but in short, that method checks all the components of the GameObject for the given Class. So calling GetComponent<Camera>() in a script that is attached to "Main Camera" in this case, means it will return the Camera component you see in the screenshot above.

This concept of GameObjects having components is really important to understand when developing in Unity, so make a mental note.

Next, I calculate and set _cameraClipDistance by subtracting the Clipping Plane Far, from the Clipping Plane Near. I use this down the line to find the centre of what the camera captures.

Additionally, I set the position of the camera to _cameraPosition. Remember, I want this script to be flexible so that no matter where I place the camera, it works.

I calculated the middle point of what the camera captures. The X and Y coordinates are the same as the camera itself, but, the "captured area" is a bit further on the Z axis. So to calculate this correctly, I start from the _cameraPosition.z, add _mainCamera.NearClipPlane to it, and then add half of the _cameraClipDistance to it.

It is probably easier to understand if I visualise it, so pardon my drawing skills but this should clear it up:

Visualisation and clarification on how to calculate the centre point of what the main camera captures in Unity.

A (0,0,0) is the "Main Camera" GameObject. B(0,0,20) is the centre of what the camera captures, currently, my "Fish" GameObject is there. The blue line equals the value of _mainCamera.NearClipPlane and green that of half of _cameraClipDistance .

Then is the _screenSize. The ScreenToWorldPoint method takes a screen space point (in pixels) and returns a corresponding world space point in the game, taking into account the position and perspective of the camera. In the first line, the distance between two points on the screen is calculated: the point in the top-left corner (0,0) and the point on the right side of the screen (Screen.width,0). This spans the width of the screen and I stored that in _screenSize.x. I did the same for the height of the screen, storing the value in _screenSize.y.

The final method in Start() is CreateColliders() in which I call one method per collider. I will only have a look at the first one, as the other ones are the same, with slightly different calculations.

/// <summary>
/// Creates the top collider.
/// </summary>
private void CreateTopCollider()
{
    _topCollider = new GameObject
    {
        name = "Top Collider"
    };
    _topCollider.gameObject.AddComponent<BoxCollider>();
    _topCollider.transform.parent = transform;
    _topCollider.transform.localScale = new Vector3(_screenSize.x, _colliderDepth, _cameraClipDistance);
    _topCollider.transform.position = new Vector3(_cameraPosition.x, _cameraPosition.y + (_screenSize.y / 2.0f) + (_topCollider.transform.localScale.y / 2.0f), _cameraClipCenter.z);
    }

First, I create a new GameObject and set the name attribute. This is the same as right-clicking in the Unity editor and creating an empty GameObject and then setting the name. Next, I added a BoxCollider component. Finally, there are a few transform-related things I changed. I set the transform of the _topCollider to the transform this script is attached to (The "Main Camera"). Then I adjusted the localScale, which you can somewhat compare to the size here, as it scales the GameObject relative to the parent. This requires a Vector3, and since I wanted it to span the entire screen, the X value can simply be the width of the screen, which I calculated earlier and saved in _screenSize.x. Since this is the top collider, the Y value is the height (or thickness) of the collider (It protrudes up, from the edge of the camera), which is the constant we defined at the start, _colliderDepth. The Z value is how "deep" the collider needs to be, and that is of course as far as the clipping planes' values, so I used _cameraClipDistance.

Ok, so now the collider has the right size. But it also needs to be in the right position. This also requires a Vector3, and while the X and Z values are simply those of _cameraPosition the Y value is a bit more involved. I started with the _cameraPosition.y and added half the screen size to it. But that was not quite right, and after some puzzling, I realised I was missing half of the "thickness" of the collider itself. That was not too hard to solve once I figured it out, so I also added _topCollider.transform.localScale.y / 2.0f to it.

As I said, the other colliders work the same, but the calculations on scale and position are of course, a bit different. So what does it do? Well, let us find out. Make sure your script is saved and attached as a component to "Main Camera". Click the play button, and pause once it is running. If you then navigate to your scene view (With the game paused), and select the "Main Camera", you can see 6 wireframe boxes encapsulating that which your camera captures! Success ๐ŸŽ‰!

Visual representation of 6 colliders encapsulating what the camera sees in the Unity editor.

Documenting your code ๐Ÿ“ƒ

At this stage, you may have noticed the comments throughout the code. Some comments have 2 slashes (//), and others have 3 (///). So what is the difference?

Not that much really. Both of the comments are ignored when the code is compiled, so it is only there for the developers. Double-slashed comments are regular comments and do not have other functions, other than informing the developer. Triple-slashed comments, on the other hand, are a C# convention for XML Documentation Comments. Using these has two primary benefits:

  1. You can use tools to generate documentation based on your comments.

  2. In editors (Technically it is an IDE) like Visual Studio, it helps with code completion/hinting and tooltip documentation.

It is good practice to document clearly and concisely as you go. Yes, you still know what the code is supposed to do, but sometimes, you are not the only one working on the project. Also, what if it has changed a couple of times, do you then remember all your code, without having to do some digging? Additionally, if you clearly explain what code and methods are supposed to do, you tend to write cleaner code, as you will split up methods to stick to the original purpose. It takes a little bit of your time, but you will thank yourself in the end for doing so.

Well done ๐Ÿ‘

We have successfully created colliders based on the size and position of our camera! Do not forget to checkin your changes in Plastic, as I showed you in the previous article!

As always, please be so kind to leave any questions or feedback you have down below! I am still very new to technical writing and Unity, so if you had issues following along, or have tips on what I can improve, let me know!

Did you find this article valuable?

Support BFranse by becoming a sponsor. Any amount is appreciated!

ย