godot 4.3 courses
Start below launch price for a limited time
Now in Early Access!

Dynamic camera targets

intermediate

By: Rafael Rodríguez - October 25, 2020

In this tutorial, you will learn to make the camera dynamically switch between following a character and anchoring to a given location. You will:

  • Toggle anchoring the camera to the player or a fixed place when entering and leaving specific areas.
  • Use steering behaviors to animate the camera’s zoom and position smoothly.

You can find the full project here.

How it’s done

In short, we have a camera that follows a target point using the arrive steering behavior, a vector math formula that makes an object smoothly move towards a target. The target can be the character the camera is attached to or any global position.

When entering and leaving specific areas, we change the camera’s target.

The code’s structure

We created three nodes to build the system:

  1. AnchorCamera2D, the camera we attach to the player. Although we keep it as a child of the player, we set it to be a top-level node, so it moves independently of its parent. It smoothly follows the player by default unless the ship enters an anchor area.
  2. Anchor2D, areas that work as anchors. When the player enters this Area2D, the camera’s target becomes this node’s location and zoom level.
  3. AnchorDetector2D, is an area node we attach to the player to detect when it enters or exits an anchor area.

Collision layers

We set these three 2D physics layers in Project -> Project Settings -> 2D Physics: actors, obstacles, and anchors.

2D Physics layers

These layers make it easier to manage collisions as the AnchorDetector2D should only detect areas in the anchors layer.

The anchor area

Let’s start with the anchor area as the detector and camera depend on it.

Create a new scene with a root Area2D node named Anchor2D with a CollisionShape2D as its child. The collision shape defines the anchor area. We also used a Sprite node to visualize the area’s bounds when running the game.

Anchor Area2D scene

Set the Anchor2D’s Collision -> Layer to anchors only and turn off Monitoring and any Collision -> Mask. Doing so makes the anchor detectable, but it doesn’t detect other physics bodies and areas itself.

Collision shape size

We want our anchors to be rectangular areas. To that end, add a RectangleShape2D to the CollsionShape2D’s Shape property. You can open the resource and set its Extents to half the screen resolution, so the area covers one screen by default. We set it to 960x540 as our project has a resolution of 1920x1080.

Anchor Area2D properties

Attach a new script to Anchor2D with the following code:

class_name Anchor2D
extends Area2D

# The camera's target zoom level while in this area.
export var zoom_level := 1.0

The anchor detector

We’ll use another area to detect anchors. Create a new scene with another Area2D node as root, this time name it AnchorDetector2D. Add a CollisionShape2D node as a child.

Anchor Detector scene

The size of the CollisionShape2D should be a little smaller than the CollisionShape2D of the Player.

Anchor Detector size

This one is going to monitor for anchor areas. Select AnchorDetector2D and set its properties as follows:

  • Turn off Monitorable.
  • Turn off all Collision -> Layer.
  • Set the Collision -> Mask.

Anchor Detector properties

Connect the signals area_entered and area_exited of the AnchorDetector2D to itself. We will use this to detect when it enters or leaves an Anchor2D area.

Signal connections

When the node enters or leaves an Anchor2D, it will emit the signal anchor_detected or anchor_detached, respectively, that we will listen to on the camera. Attach a script to the AnchorDetector2D.

class_name AnchorDetector2D
extends Area2D

# Emitted when entering an anchor area.
signal anchor_detected(anchor)
# Emitted after exiting all anchor areas.
signal anchor_detached


func _on_area_entered(area: Anchor2D) -> void:
	emit_signal("anchor_detected", area)


# When exiting an area, we have to ensure we're not entering another anchor.
func _on_area_exited(area: Anchor2D) -> void:
	var areas: Array = get_overlapping_areas()
	# To do so, we check that's there's but one overlapping area left and that it's
	# the one passed to this callback function.
	if get_overlapping_areas().size() == 1 and area == areas[0]:
		emit_signal("anchor_detached")

The camera

Create a new scene with a Camera2D node as root and name it AnchorCamera2D. In the Inspector, set the camera node as Current, so Godot uses it as our game’s camera.

Camera2D is current

Attach a script to the AnchorCamera2D with the following code:

class_name AnchorCamera2D
extends Camera2D

# Distance to the target in pixels below which the camera slows down.
const SLOW_RADIUS := 300.0

# Maximum speed in pixels per second.
export var max_speed := 2000.0
# Mass to slow down the camera's movement
export var mass := 2.0

var _velocity = Vector2.ZERO
# Global position of an anchor area. If it's equal to `Vector2.ZERO`,
# the camera doesn't have an anchor point and follows its owner.
var _anchor_position := Vector2.ZERO
var _target_zoom := 1.0


func _ready() -> void:
	# Setting a node as top-level makes it move independently of its parent.
	set_as_toplevel(true)


# Every frame, we update the camera's zoom level and position.
func _physics_process(delta: float) -> void:
	update_zoom()

	# The camera's target position can either be `_anchor_position` if the value isn't
	# `Vector2.ZERO` or the owner's position. The owner is the root node of the scene in which we
	# instanced and saved the camera. In our demo, it's the Player.
	var target_position: Vector2 = (
		owner.global_position
		if _anchor_position.is_equal_approx(Vector2.ZERO)
		else _anchor_position
	)

	arrive_to(target_position)


# Entering in an `Anchor2D` we receive the anchor object and change our `_anchor_position` and
# `_target_zoom`
func _on_AnchorDetector2D_anchor_detected(anchor: Anchor2D) -> void:
	_anchor_position = anchor.global_position
	_target_zoom = anchor.zoom_level


# Leaving the anchor the zoom return to 1.0 and the camera's center to the player
func _on_AnchorDetector2D_anchor_detached() -> void:
	_anchor_position = Vector2.ZERO
	_target_zoom = 1.0


# Smoothly update the zoom level using a linear interpolation.
func update_zoom() -> void:
	if not is_equal_approx(zoom.x, _target_zoom):
		# The weight we use considers the delta value to make the animation frame-rate independent.
		var new_zoom_level: float = lerp(
			zoom.x, _target_zoom, 1.0 - pow(0.008, get_physics_process_delta_time())
		)
		zoom = Vector2(new_zoom_level, new_zoom_level)


# Gradually steers the camera to the `target_position` using the arrive steering behavior.
func arrive_to(target_position: Vector2) -> void:
	var distance_to_target := position.distance_to(target_position)
	# We approach the `target_position` at maximum speed, taking the zoom into account, until we
	# get close to the target point.
	var desired_velocity := (target_position - position).normalized() * max_speed * zoom.x
	# If we're close enough to the target, we gradually slow down the camera.
	if distance_to_target < SLOW_RADIUS * zoom.x:
		desired_velocity *= (distance_to_target / (SLOW_RADIUS * zoom.x))

	_velocity += (desired_velocity - _velocity) / mass
	position += _velocity * get_physics_process_delta_time()

Creating the Player scene

We designed a player-controlled ship to test our camera for this small demo. It’s a KinematicBody2D node with the following code attached to it:

# Ship that rotates and moves forward, similar to the game classic Asteroid.
class_name Player
extends KinematicBody2D

export var speed := 520
export var angular_speed := 3.0


func _physics_process(delta):
	var direction := Input.get_action_strength("right") - Input.get_action_strength("left")
	var velocity = Input.get_action_strength("move") * transform.x * speed
	rotation += direction * angular_speed * delta
	move_and_slide(velocity)

To control the Player’s movement, we defined the following input actions in Project -> Project Settings… -> Input Map: right, left, and move.

Screenshot of the input map window with the actions

The AnchorCamera2D should be a child of our Player to follow it by default, using the owner variable. To detect Anchor2D nodes, we also instantiate AnchorDetector2D.

Player scene

We need to connect the signals anchor_detected and anchor_detached from AnchorDetector2D to the methods on_AnchorDetector2D_anchor_detected and on_AnchorDetector2D_anchor_detached of AnchorCamera2D.

Connection of signals <em>anchor_detected</em> and <em>anchor_detached</em>

And that is it!

With the connections done and some anchor areas in the level, the camera dynamically moves between the player and other areas of interest.

Made by

Our tutorials are the result of careful teamwork to ensure we produce high quality content. The following team members worked on this one:

Rafael Rodríguez

Tutor

Nathan Lovato

Founder

Related courses

Banner image

Learn 2D Gamedev with Godot 4 $99.95

Built on the success of GDQuest’s bestselling course, this Godot 4 course uses cutting edge ed-tech to teach the fundamentals of game development over 19 modules packed with interactive exercises.

Banner image

Learn 3D Gamedev with Godot 4 $99.95

This long-awaited course adds a third dimension to the knowledge you pick up in Learn 2D Gamedev. By the end of it, you will have the skills to build your own 3D game.