Custom Camera2D in Godot

A camera in a game can be understood as the framing through which you’re seeing the world. That’s a good way to think about what’s happening, but personally, it’s not the best mental model to use when thinking about and coding the transformations that will position the player, sprites, and user interface on the game window.

We can think about it as having two rectangles, one representing the game window and the other the game content. We want to translate and scale the game content rectangle so that it fills the game window. It’s the same idea as having an image fit or fill an area of the screen.

How do we transform the content rectangle (content_rect) to make it fit inside the view rectangle (view_rect)?

func get_transform_to_fit(
	content_rect: Rect2, view_rect: Rect2) -> Transform2D:
  1. First, we find how much we need to scale the content_rect to have it fit inside the view_rect. We want it to fit inside without being stretched, so we use the minimum value from the axis of the difference between the rectangle sizes.
	var scale: Vector2 = view_rect.size / content_rect.size
	var min_scale: float = min(scale.x, scale.y)
	scale = Vector2(min_scale, min_scale)
  1. We now need to know how much to move content_rect so that it gets positioned inside the view_rect. We start by taking the difference between the position of both rectangles so that after translation both are resting at the same position (which here is the top-left corner).
	var translation: Vector2 = content_rect.position - view_rect.position
  1. Our rectangles may not have the same aspect ratio, and we want our content_rect to be centered inside the view_rect. To do that, we calculate the scaled size of the content rectangle and add to the translation half of the difference between that size and the view_rect.size.
	var scaled_content_rect_size: Vector2 = content_rect.size * scale
	translation += (view_rect.size - scaled_content_rect_size) / 2
  1. To finish, we create and return a Transform2D that uses that scale and translation.
	var transform = Transform2D(0, scale, 0, translation)
	return transform

How can we use that to write code that works like a custom Camera2D? Consider a level of a 2D platformer where we want the camera to follow the player. We need a rectangle that represents the game window and a rectangle to represent what part of the level we want to fit inside the game window. After we get that transformation, we can apply it to the whole level.

  1. We create a rectangle to represent what the camera is seeing, and get a reference to our player character. We can get a Rect2 representing the viewport by calling get_viewport_rect() from a class that extends CanvasItem. We then center the cam_rect around the player.
var cam_rect := Rect2(Vector2.ZERO, Vector2(854, 480))
@onready var player: Node2D = $Player

func apply_camera():
	var viewport_rect: Rect2 = get_viewport_rect()
	cam_rect.position = player.position - cam_rect.size / 2
  1. We use those two rectangles to calculate a transform that will make cam_rect fit inside viewport_rect.
	var cam_transform := get_transform_to_fit(cam_rect, viewport_rect)
  1. Where to apply that transform? We could apply it to a Node2D that’s the parent of all nodes used in our level. The main problem with that is that collision shapes used in physics bodies won’t work as expected if they’re scaled. By applying our transform to a node that directly or indirectly has collision shapes nodes, we will be scaling them and breaking the physics of our game. An alternative is to apply it to the Viewport.canvas_transform.
	var viewport = get_viewport()
	viewport.canvas_transform = cam_transform

The viewport canvas_transform is applied by the rendering server before the CanvasItem are drawn to the screen, it does not change the global position or scale of the nodes, and because of that does not affect the data used by the physics server.

That canvas_transform applies to all CanvasItem on the default canvas layer. So if you don’t want your UI to be scaled and moved around depending on the position of the player, or if you want a fixed background, you can put those as a child of a CanvasLayer. That will, behind the scenes, create a new canvas layer in the RenderingServer with its own canvas transform.

By directly controlling the position of our cam_rect, it opens the possibility to fine-tune how and when it moves around in relation to the player, or even multiple objects.

Published 2023.08.12

Wanna know when I release a new game?