How could complex 2D camera boundaries be made

Godot Version

4.6.2

Question

So I was trying to improve my camera on my metroidvania but then I realized that Camera2D doesnt support complex limits. like limits that are based on a 2D polygon or tilemap, plugins like PhantomCamera also dont support it and rectangular limits give a lot of useless space. I could just split it into multiple rectangular zones but that doesnt feel natural

Some people suggested me to use Paths, but how? And im also trying to keep the phantomcameras on my game because they have some good features and also becaus ethe scope is big so removing it will break a lot of things :sweat_smile:

What are you trying to accomplish with these complex limits?

Camera Boundaries that are not rectangles, to not show empty space. Like on this room, I would only show the high pillar and the hallway at the top and not the big black space

Seems to me this can be accomplished with rectangles.

When the y position is in the hallway area, use a rect that covers the entire area.

Below that y position, use a rect that shows just the pillar area.

I grant that transitioning between the two could be finicky - but I suspect that figuring that out will be simpler than getting the camera to work with a complex polygon.

This was just a simple example, there are some more complex scenes where it even needs to be diagonal. I really cant use rectangles

Use a polygon. Check if camera’s origin is inside via Geometry2D::is_point_in_polygon(). If not, iterate through polygon edges, find the edge point nearest to the camera using Geometry2D::get_closest_point_to_segment() and set the camera position to that.

I haven’t used this so I can’t vouch for it, but this project might have what you need: GitHub - daz-b-like/ProCam2D_Godot3.x: The ultimate camera for all your 2D needs. · GitHub

Specifically this feature: PCamRoom - “Constrains the camera to an area it covers.”

If nothing else, the project may give you the insights you need to resolve your issue.

I think building one myself would be better so I can give it the needed support without breaking anything. the project is quite big

I understand - I tend to use other people’s solutions as inspiration for my own.

Okay so I tried for 2 hours but kept failing it didnt go well with PhantmCamera, people suggested using a Path based camera but how can it be made and how does it even work?

So I have good news and bad news.

Good News

The good news is I have the plugin for you! My Camera2D Plugin gives you a bunch of Component nodes that can just be added to your Camera2D node in the editor or during gameplay and they just work.

Specifically, the Camera2DLimit node.

image

You can just assign a TileMapLayer to it in the editor:

image

Or more likely for your use, you can just emit a signal when you load a level:

Camera2DSignalBus.update_camera_boundaries.emit(tile_map_layer)

It also has a set_tile_map_layer_limit() function where you can directly pass one to it, and set_limit() function that accepts a Rect2i to set the boundaries.

Bad News

Now the bad news. You cannot use a Polygon2D to set boundaries on a Camera2D.

You must use the left, top, right and bottom values if you want to use the Camera2D Limit feature.

Solutions

In your example map, you have tiles that go all the way around the edge. Camera2DLimit measures the used tiles of your TileMapLayer when you pass it, and uses that for the camera boundaries. So all that black space is going to be included, because you have an edge of tils around the whole thing. (Though even if not it describes a single Rect2i around all the used tiles on the map.)

I see you have what looks like a secret room on there. If you don’t want that room triggered until the player shoots a door or something, you’ll need a trigger. Here’s what I would do, using my plugin:

I would create a custom Area2D call it like CameraZone2D.

class_name CameraZone2D extends Area2D


func _ready() -> void:
	body_entered.connect(_on_camera_zone_activated)
	area_entered.connect(_on_camera_zone_activated)


func _on_camera_zone_activated(_node: Node2D) -> void:
	for collision_shape_2d in get_children():
		if collision_shape_2d is CollisionShape2D:
			if collision_shape_2d.shape is RectangleShape2D:
				Camera2DSignalBus.update_camera_limit.emit(collision_shape_2d.shape.get_rect())

I’d add second signal:

#Camera2DSignalBus Autoload
extends Node

## Any camera listening can take the passed TimeMapLayer to update itself,
## so that nothing outside the used map tiles will be shown.
signal update_camera_boundaries(tile_map_layer: TileMapLayer)

## Any camera listening can take the passed Rect2i to update itself.
signal update_camera_limit(rect: Rect2i)

And hook that signal in:

@tool
@icon("res://addons/dragonforge_camera_2d/assets/textures/icons/limit.svg")
## Adding this component to a [Camera2D] restricts it to the to the used area of
## a [TileMapLayer] or [Rect2i].
class_name Camera2DLimit extends Camera2DComponent

## The [TileMapLayer] to use to restrict the atttached [Camera2D]'s limits.
@export var tile_map_layer: TileMapLayer:
	set(value):
		tile_map_layer = value
		if _is_camera_ready and tile_map_layer:
			set_tile_map_layer_limit(tile_map_layer)
		update_configuration_warnings()

var _is_camera_ready = false


func _ready() -> void:
	get_parent().ready.connect(_on_parent_ready)
	if not Camera2DSignalBus.is_node_ready(): 
		await Camera2DSignalBus.ready
	Camera2DSignalBus.update_camera_boundaries.connect(set_tile_map_layer_limit)
	Camera2DSignalBus.update_camera_limit.connect(set_limit)


func _on_parent_ready() -> void:
	_enter_tree()


## Sets the attached camera's boundaries to be that of the passed [Rect2i]
## [param limit].
func set_limit(limit: Rect2i) -> void:
	camera.limit_top = limit.position.y
	camera.limit_left = limit.position.x
	camera.limit_bottom = limit.end.y
	camera.limit_right = limit.end.x


## Sets the attached camera's boundaries to be that of the used tiles on the
## passed [param tile_map_layer]. [b]NOTE:[/b] It is recommended to use
## [signal Camera2DSignalBus.update_camera_boundaries] to trigger this
## functionality.
func set_tile_map_layer_limit(tile_map_layer: TileMapLayer) -> void:
	var map_boundaries: Rect2i = tile_map_layer.get_used_rect()
	var tile_size: Vector2i
	tile_size.x = tile_map_layer.tile_set.tile_size.x
	tile_size.y = tile_map_layer.tile_set.tile_size.y
	map_boundaries.position *= tile_size
	map_boundaries.size *= tile_size
	set_limit(map_boundaries)


# Configure limits if the node is added during execution instead of in the
# editor.
func _enter_tree() -> void:
	super()
	if camera is Camera2D:
		_is_camera_ready = true
	if tile_map_layer:
		set_tile_map_layer_limit(tile_map_layer)


# Reset camera limits in case we are removing this node from the tree.
func _exit_tree() -> void:
	if camera is Camera2D:
		camera.limit_top = -10000000
		camera.limit_left = -10000000
		camera.limit_bottom = 10000000
		camera.limit_right = 10000000


func _get_configuration_warnings() -> PackedStringArray:
	var warnings: PackedStringArray = []
	if not camera is Camera2D:
		warnings.append("Camera2DLimit only serves to provide modifications to Camera2D derived nodes. Please only use it as a child of a Camera2D to modify it.")
	if not tile_map_layer:
		warnings.append("A tile map layer must be provided for Camera2DLimit to function. Please attach a TileMapLayer to it!")
	return warnings

Using CameraZone2D

You could then create CameraZone2D nodes, give them rectangle shapes and cover your pillar with one, and your corridor with the other, and a third for your secret room. If the switch is too jumpy, turn on Smoothing Under the Camera2D’s Limit section. Make sure they only have a mask for the player physics layer, and you’re in business.

Then if that’s not enough, you can get fancy…

Updating CameraZone2D for Other Shapes

It’s easy enough to extend the _on_camera_zone_activated() function at this point. Add an elif, and check for a CollisionPolygon2D. Then turn on _physics_process() if it’s a CollisionPolygon2D.

The hard part comes when you have to detect the bounds of the CollisionPolygon2D every frame by shooting rays out in four directions to create the bounding box. Maybe @normalized has some thoughts on that using get_closest_point_to_segment().

But here’s some code to get you started. Everything should work except for the last bit.

class_name CameraZone2D extends Area2D

var player: CharacterBody2D


func _ready() -> void:
	body_entered.connect(_on_camera_zone_activated)
	area_entered.connect(_on_camera_zone_activated)
	body_exited.connect(_on_camera_zone_deactivated)
	area_exited.connect(_on_camera_zone_deactivated)
	set_physics_process(false)


func _physics_process(_delta: float) -> void:
	player.position
	var bounding_rectangle: Rect2i
	# Create four rays from the player position and hit the four edges of the
	# Polygon2D.
	# Use those collisions to create the dimensions of the bounding_rectangle
	# Pass it every frame
	Camera2DSignalBus.update_camera_limit.emit(bounding_rectangle)


func _on_camera_zone_activated(node: Node2D) -> void:
	for collision_shape in get_children():
		if collision_shape is CollisionShape2D:
			if collision_shape.shape is RectangleShape2D:
				Camera2DSignalBus.update_camera_limit.emit(collision_shape.shape.get_rect())
		elif collision_shape is CollisionPolygon2D:
			if node is CharacterBody2D:
				player = node
				set_physics_process(true)


func _on_camera_zone_deactivated(node: Node2D) -> void:
	if player:
		player = null
		set_physics_process(false)
1 Like

This is actually really cool, but there is no secret on that black spot just 2 transitions to the next room. But before trying this out il first try experimenting with paths first because PhantomCamera2D has it and because its not limited like tilemaps.

But if I fail, I’ll give your plugin a shot! Thanks

1 Like

Well the other two components you might be interested in are the Camera2DZoom and Camera2DShake nodes. There’s also a Camera2DPinchZoom for mobile games. For the shake node I added in controller shake functionality in the same node.

Don’t use PhantomCamera then.

Start by constraining a single point. Once you get this working you can attach anything to that point, including the phantom camera.

1 Like

What do you mean?

Your problem can be reduced to constraining a single point to a polygon. If you solve that, the rest of it is trivial. And constraining a point to a polygon can be done as I described above.

1 Like

So I wass able to add polygon based limits tho they acted a bit weird, so I went with this but Im not getting the camera limits to work. I added a tilemapLayer that doesnt cover that black part but I can still see that black part, I can still go over the limit

Well, nvm. I was able to create polygon based limits by updating camera limits based on the camera’s position and polygon. I feel like some people might need it aswell so I’ll be making it an adddon and it does support PhantomCamera2D

2 Likes