Flexible camera

I didn't use this code in HONK but it helps me to make this flexible camera for Game Maker 2.

Here is an original devlog.

Hello everybody, I'm carrying on working on the update. Recently I finished developing a new camera system which is very flexible and easy to use.

Display manager

However, before starting working on the camera system, I had made a pixel perfect display manager.

It based on a Game resolution tutorial series by PixelatedPope:

I highly recommend checking this tutorial because it's superb and explains a lot of important moments.

And here is my version of display manager. There aren't significant differences with display manager by PixelatedPope, instead of variables names.

Camera

After implementing a display manager, I started working on a new camera. An old version was pretty simple and not so flexible as I wanted.

I use this tutorial by FriendlyCosmonaut as a start point. This tutorial is fantastic, as it shows how to make a flexible camera controller which can be useful everywhere. Nonetheless, I've made some changes to make this camera system a little bit better for my project.

I wanted to make smooth camera for some camera modes; I needed game pad support; Let's dive in it!

CREATE EVENT

``` /// @description Camera parameters

// Main settings global.Camera = id;

// Macroses

macro mainCamera view_camera[0]

macro cameraPositionX cameragetview_x(mainCamera)

macro cameraPositionY cameragetview_y(mainCamera)

macro cameraWidth cameragetview_width(mainCamera)

macro cameraHeight cameragetview_height(mainCamera)

// User events

macro ExecuteFollowObject event_user(0)

macro ExecuteFollowBorder event_user(1)

macro ExecuteFollowPointPeek event_user(2)

macro ExecuteFollowDrag event_user(3)

macro ExecuteMoveToTarget event_user(4)

macro ExecuteMoveToFollowObject event_user(5)

macro ExecuteMoveWithGamepad event_user(6)

macro ExecuteMoveWithKeyboard event_user(7)

macro ClampCameraPosition event_user(8)

macro ExecuteCameraShake event_user(9)

// Transform cameraX = x; cameraY = y;

cameraOriginX = cameraWidth * 0.5; cameraOriginY = cameraHeight * 0.5;

// Cameramodes enum CameraMode { FollowObject, FollowBorder, FollowPointPeek, FollowDrag, MoveToTarget, MoveToFollowObject }

cameraMode = CameraMode.MoveToTarget; clampToBorders = false;

// Follow parameters cameraFollowTarget = objplayer; targetX = roomwidth / 2; targetY = room_height / 2; isSmooth = true;

mousePreviousX = -1; mousePreviousY = -1;

cameraButtonMoveSpeed = 5; // Only for gamepad and keyboard controls cameraDragSpeed = 0.5; // Only for CameraMode.FollowDrag cameraSpeed = 0.1;

// Camera shake parameters cameraShakeValue = 0; angularShakeEnabled = false; // Enables angular shaking

// Zoom parameters cameraZoom = 0.65; cameraZoomMax = 4; ```

STEP EVENT

This is a state machine of the camera. You can easily modify it without any problems because all logic of each mode contains in separate user events.

``` /// @description Camera logic

cameraOriginX = cameraWidth * 0.5; cameraOriginY = cameraHeight * 0.5;

cameraX = cameraPositionX; cameraY = cameraPositionY;

switch (cameraMode) { case CameraMode.FollowObject: ExecuteFollowObject; break;

case CameraMode.FollowBorder:
    ExecuteFollowBorder;
break;

case CameraMode.FollowPointPeek:
    ExecuteFollowPointPeek;
break;

case CameraMode.FollowDrag:
    ExecuteFollowDrag;
break;

case CameraMode.MoveToTarget:
    ExecuteMoveToTarget;
break;

case CameraMode.MoveToFollowObject:
    ExecuteMoveToFollowObject;
break;

}

ClampCameraPosition;

ExecuteCameraShake;

camerasetview_pos(mainCamera, cameraX, cameraY); ```

USER EVENTS

Why do I use user_events? I don't like creating a lot of exclusive scripts for one object, and user events are a good place to avoid this problem and store exclusive code for objects.

Secondly, it's really easy to make changes in user event than in all sequence, in this case, I'm sure that I won't break something else because of my inattentiveness.

``` ///----------------------------------------------/// /// User Event 0 /// ///----------------------------------------------///

/// @description FollowObject

var targetExists = instanceexists(cameraFollowTarget);

if (_targetExists) { targetX = cameraFollowTarget.x; targetY = cameraFollowTarget.y;

CalculateCameraDelayMovement();

}

///----------------------------------------------/// /// User Event 1 /// ///----------------------------------------------///

/// @description FollowBorder

switch (global.CurrentInput) { case InputMethod.KeyboardMouse: var _borderStartMargin = 0.35; var _borderEndMargin = 1 - _borderStartMargin;

    var _borderStartX = cameraX + (cameraWidth * _borderStartMargin);
    var _borderStartY = cameraY + (cameraHeight * _borderStartMargin);

    var _borderEndX = cameraX + (cameraWidth * _borderEndMargin);
    var _borderEndY = cameraY + (cameraHeight * _borderEndMargin);

    var _isInsideBorder = point_in_rectangle(mouse_x, mouse_y, _borderStartX, _borderStartY, _borderEndX, _borderEndY);

    if (!_isInsideBorder)
    {
        var _lerpAlpha = 0.01;

        cameraX = lerp(cameraX, mouse_x - cameraOriginX, _lerpAlpha);
        cameraY = lerp(cameraY, mouse_y - cameraOriginY, _lerpAlpha);
    }
    else
    {
        ExecuteMoveWithKeyboard;
    }
break;

case InputMethod.Gamepad:
    ExecuteMoveWithGamepad;
break;

}

///----------------------------------------------/// /// User Event 2 /// ///----------------------------------------------///

/// @description FollowPointPeek

var _distanceMax = 190; var _startPointX = cameraFollowTarget.x; var _startPointY = cameraFollowTarget.y - cameraFollowTarget.offsetY - cameraFollowTarget.z;

switch (global.CurrentInput) { case InputMethod.KeyboardMouse: var direction = pointdirection(startPointX, _startPointY, mousex, mousey); var _aimDistance = pointdistance(startPointX, _startPointY, mousex, mousey); var _distanceAlpha = min(aimDistance / _distanceMax, 1); break;

case InputMethod.Gamepad:
    var _axisH = gamepad_axis_value(global.ActiveGamepad, gp_axisrh);
    var _axisV = gamepad_axis_value(global.ActiveGamepad, gp_axisrv);
    var _direction = point_direction(0, 0, _axisH, _axisV);
    var _distanceAlpha = min(point_distance(0, 0, _axisH, _axisV), 1);
break;

}

var distance = lerp(0, _distanceMax, _distanceAlpha); var _endPointX = _startPointX + lengthdirx(distance, _direction) var _endPointY = _startPointY + lengthdiry(_distance, _direction)

targetX = lerp(startPointX, _endPointX, 0.2); targetY = lerp(startPointY, _endPointY, 0.2);

CalculateCameraDelayMovement();

///----------------------------------------------/// /// User Event 3 /// ///----------------------------------------------///

/// @description FollowDrag

switch (global.CurrentInput) { case InputMethod.KeyboardMouse: var mouseClick = mousecheckbutton(mbright);

    var _mouseX = display_mouse_get_x();
    var _mouseY = display_mouse_get_y();

    if (_mouseClick)
    {
        cameraX += (mousePreviousX - _mouseX) * cameraDragSpeed;
        cameraY += (mousePreviousY - _mouseY) * cameraDragSpeed;
    }
    else
    {
        ExecuteMoveWithKeyboard;
    }

    mousePreviousX = _mouseX;
    mousePreviousY = _mouseY;
break;

case InputMethod.Gamepad:
    ExecuteMoveWithGamepad;
break;

}

///----------------------------------------------/// /// User Event 4 /// ///----------------------------------------------///

/// @description MoveToTarget

MoveCameraToPoint(cameraSpeed);

///----------------------------------------------/// /// User Event 5 /// ///----------------------------------------------///

/// @description MoveToFollowObject

var targetExists = instanceexists(cameraFollowTarget);

if (_targetExists) { targetX = cameraFollowTarget.x; targetY = cameraFollowTarget.y;

MoveCameraToPoint(cameraSpeed);

var _distance = point_distance(cameraX, cameraY, targetX - cameraOriginX, targetY - cameraOriginY);

if (_distance < 1)
{
    cameraMode = CameraMode.FollowObject;
}

}

///----------------------------------------------/// /// User Event 6 /// ///----------------------------------------------///

/// @description MoveWithGamepad

var axisH = gamepadaxisvalue(global.ActiveGamepad, gpaxisrh); var axisV = gamepadaxisvalue(global.ActiveGamepad, gpaxisrv);

var direction = pointdirection(0, 0, axisH, _axisV); var _lerpAlpha = min(pointdistance(0, 0, _axisH, _axisV), 1); var _speed = lerp(0, cameraButtonMoveSpeed, _lerpAlpha);

cameraX += lengthdirx(speed, direction); cameraY += lengthdiry(_speed, _direction);

///----------------------------------------------/// /// User Event 7 /// ///----------------------------------------------///

/// @description MoveWithKeyboard

var directionX = objgameManager.keyMoveRight - objgameManager.keyMoveLeft; var _directionY = objgameManager.keyMoveDown - obj_gameManager.keyMoveUp;

if (directionX != 0 || _directionY != 0) { var _direction = pointdirection(0, 0, _directionX, _directionY);

var _speedX = lengthdir_x(cameraButtonMoveSpeed, _direction);
var _speedY = lengthdir_y(cameraButtonMoveSpeed, _direction);

cameraX += _speedX;
cameraY += _speedY;

}

///----------------------------------------------/// /// User Event 8 /// ///----------------------------------------------///

/// @description ClampCameraPosition

if (clampToBorders) { cameraX = clamp(cameraX, 0, roomwidth - cameraWidth); cameraY = clamp(cameraY, 0, roomheight - cameraHeight); }

///----------------------------------------------/// /// User Event 9 /// ///----------------------------------------------///

/// @description CameraShaker

// Private parameters var _cameraShakePower = 5; var _cameraShakeDrop = 0.1; var _cameraAngularShakePower = 0.5;

// Shake range calculations var _shakeRange = power(cameraShakeValue, 2) * _cameraShakePower;

// Add shakeRange to camera position cameraX += randomrange(-shakeRange, _shakeRange); cameraY += randomrange(-_shakeRange, _shakeRange);

// Chanege view angle to shake camera angle if angularShakeEnabled { camerasetviewangle(mainCamera, randomrange(-_shakeRange, _shakeRange) * _cameraAngularShakePower); }

// Decrease shake value if cameraShakeValue > 0 { cameraShakeValue = max(cameraShakeValue - _cameraShakeDrop, 0); } ```

ADDITIONAL SCRIPTS

In order to make my life a little bit easier, I use some scripts too. But be aware CalculateCameraDelayMovement and MoveCameraToPoint are used exclusively in camera code.

You should use SetCameraMode to change camera mode and SetCameraZoom to change camera zoom.

``` ///----------------------------------------------/// /// CalculateCameraDelayMovement /// ///----------------------------------------------///

var _x = targetX - cameraOriginX; var _y = targetY - cameraOriginY;

if (isSmooth) { var _followSpeed = 0.08;

cameraX = lerp(cameraX, _x, _followSpeed);
cameraY = lerp(cameraY, _y, _followSpeed);

} else { cameraX = _x; cameraY = _y; }

///----------------------------------------------/// /// MoveCameraToPoint /// ///----------------------------------------------///

/// @param moveSpeed

var _moveSpeed = argument0;

cameraX = lerp(cameraX, targetX - cameraOriginX, _moveSpeed); cameraY = lerp(cameraY, targetY - cameraOriginY, _moveSpeed);

///----------------------------------------------/// /// SetCameraMode /// ///----------------------------------------------///

/// @description SetCameraMode

/// @param mode /// @param followTarget/targetX /// @param targetY

with (global.Camera) { cameraMode = argument[0];

switch (cameraMode)
{
    case CameraMode.FollowObject:
    case CameraMode.MoveToFollowObject:
        cameraFollowTarget = argument[1];
    break;

    case CameraMode.MoveToTarget:
        targetX = argument[1];
        targetY = argument[2];
    break;
}

}

///----------------------------------------------/// /// SetCameraZoom /// ///----------------------------------------------///

/// @description SetCameraZoom

/// @param newZoom

var _zoom = argument0;

with (global.Camera) { cameraZoom = clamp(zoom, 0.1, cameraZoomMax); camerasetviewsize(mainCamera, global.IdealWidth / cameraZoom, global.IdealHeight / cameraZoom); } ```

Thank you for reading!