Refactorings: Idle Pachinko Camera Bounds

I recently implemented a feature to put bounds on camera movement for the Idle Pachinko game that Matt Torres and I have been developing. When I write code, I typically write it in multiple passes like how an author of a paper or a book takes multiple editing passes over their text. Below is the code for the camera bounds feature in multiple passes, showing how I improved the quality and understandability of the code before opening a pull request and calling it done.

In this game the camera can only move up and down; it is locked to 0 on the x-axis. We have a top and a bottom boundary for the game area that we want to lock the camera to when scrolling/moving the camera up and down. The top and bottom boundary values are encapsulated by the GameBoundaries class which is used by this camera system to enforce the boundaries on the camera position.

using EcsRx.Events;
using EcsRx.Plugins.ReactiveSystems.Custom;
using MisfitLabs.IdlePachinko.InGame.Events;
using UnityEngine;

public class CameraMovementSystem : EventReactionSystem<ScreenDraggedEvent>
{
    private const float DragSpeed = 1.5f;

    private readonly Camera _camera;
    private readonly GameBoundaries _gameBoundaries;

    public CameraMovementSystem(Camera camera, GameBoundaries gameBoundaries, IEventSystem eventSystem)
        : base(eventSystem)
    {
        _camera = camera;
        _gameBoundaries = gameBoundaries;
    }

    public void EventTriggered(ScreenDraggedEvent eventData)
    {
        var newPosition = _camera.transform.position + new Vector3(0.0f, -eventData.DeltaY * DragSpeed, 0.0f);

        var orthographicSize = _camera.orthographicSize;
        var maxY = _gameBoundaries.Top - orthographicSize;
        var minY = _gameBoundaries.Bottom + orthographicSize;
        newPosition.y = Mathf.Clamp(newPosition.y, minY, maxY);

        _camera.transform.position = newPosition;
    }
}

While the above code works, there is a lot going on in the EventTriggered method. All the different calculations for moving the camera and locking it into the bounds happens here. Splitting some things out into private methods with good names will enhance the readability of this class.

Since this was a new feature to the existing CameraMovementSystem that adding boundary enforcement, I first split out that enforcement into the method ClampToGameBoundaries. Now rather than needing to read the 4 lines that do the boundary enforcement and interpret what they are doing we can read the name of this function and it is immediately clear what it is doing. We can still jump into the new method to see how it is doing the enforcement. It is often good to separate the “what” from the “how”.

using EcsRx.Events;
using EcsRx.Plugins.ReactiveSystems.Custom;
using MisfitLabs.IdlePachinko.InGame.Events;
using UnityEngine;

public class CameraMovementSystem : EventReactionSystem<ScreenDraggedEvent>
{
    private const float DragSpeed = 1.5f;

    private readonly Camera _camera;
    private readonly GameBoundaries _gameBoundaries;

    public CameraMovementSystem(Camera camera, GameBoundaries gameBoundaries, IEventSystem eventSystem)
        : base(eventSystem)
    {
        _camera = camera;
        _gameBoundaries = gameBoundaries;
    }

    public override void EventTriggered(ScreenDraggedEvent eventData)
    {
        var newPosition = _camera.transform.position + new Vector3(0.0f, -eventData.DeltaY * DragSpeed, 0.0f);
        _camera.transform.position = ClampToGameBoundaries(newPosition);;
    }

    private Vector3 ClampToGameBoundaries(Vector2 position)
    {
        var orthographicSize = _camera.orthographicSize;
        var maxY = _gameBoundaries.Top - orthographicSize;
        var minY = _gameBoundaries.Bottom + orthographicSize;
        position.y = Mathf.Clamp(position.y, minY, maxY);
        return position;
    }
}

After the 2nd pass there are still a couple things that stick out to me. The first is the orthagraphicSize value. What is that? It is half the camera’s height, which we need to enforce the boundary since the camera position is the center point, but we want to make sure the camera edge does not go passed the boundaries. I spent some time trying to name it better, but ultimately decided to leave it as is. The documentation from Unity3D – “Camera’s half-size when in orthographic mode.” – is quite clear and sometimes it is best to stick with the naming of the libraries you are using.

The second thing that sticks out to me is that there is still quite a bit of unnamed calculation happening in the EventTriggered method. To address this, I split out the camera new position dragging into a new method CalculateNewPosition and split it into 2 lines so that each step of the calculation is named – The move delta and then the new position.

using EcsRx.Events;
using EcsRx.Plugins.ReactiveSystems.Custom;
using MisfitLabs.IdlePachinko.InGame.Events;
using UnityEngine;

public class CameraMovementSystem : EventReactionSystem<ScreenDraggedEvent>
{
    private const float DragSpeed = 1.5f;

    private readonly Camera _camera;
    private readonly GameBoundaries _gameBoundaries;

    public CameraMovementSystem(Camera camera, GameBoundaries gameBoundaries, IEventSystem eventSystem)
        : base(eventSystem)
    {
        _camera = camera;
        _gameBoundaries = gameBoundaries;
    }

    public override void EventTriggered(ScreenDraggedEvent eventData)
    {
        var newPosition = CalculateNewPosition(eventData);
        _camera.transform.position = ClampToGameBoundaries(newPosition);
    }

    private Vector3 CalculateNewPosition(ScreenDraggedEvent eventData)
    {
        var moveDelta = new Vector3(0.0f, -eventData.DeltaY * DragSpeed, 0.0f);
        return _camera.transform.position + moveDelta;
    }

    private Vector3 ClampToGameBoundaries(Vector3 position)
    {
        var orthographicSize = _camera.orthographicSize;
        var maxY = _gameBoundaries.Top - orthographicSize;
        var minY = _gameBoundaries.Bottom + orthographicSize;
        position.y = Mathf.Clamp(position.y, minY, maxY);
        return position;
    }
}