Camera2D moving inverse to player

Godot Version

4.4.1

Question

So I’m making a top down RPG game and my player’s camera works fine in the overworld, but I recently implemented a shop scene which glitches out my camera. When my player moves up within the shop it looks as if the camera shifts down instead of moving up with the player. I have code to automatically detect the bounds of the tilemap as well, which doesn’t seem to be working either. I’ve messed with my camera settings but nothings worked so far. I appreciate the help!

Camera’s Code:

class_name PlayerCamera extends Camera2D

func _ready() -> void:
	LevelManager.TileMapBoundsChanged.connect(UpdateLimits)
	UpdateLimits(LevelManager.current_tilemap_bounds)


func UpdateLimits( bounds : Array[Vector2] ):
	if bounds == []:
		return
	limit_left = int( bounds[0].x )
	limit_top = int( bounds[0].y )
	limit_right = int( bounds[1].x )
	limit_bottom = int( bounds[1].y)
	pass
	print(bounds)


It looks like your issue is potentially in code you didn’t show, which is whatever is generating the Array data for the TileMapBoundsChanged signal.

This is the code I use to do the same thing:

class_name CharacterBoundCamera2D extends Camera2D

## Sets the camera's boundaries to be that of the used tiles on the passed
## TileMapLayer.
func change_camera_boundaries(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
	limit_top = map_boundaries.position.y
	limit_left = map_boundaries.position.x
	limit_bottom = map_boundaries.end.y
	limit_right = map_boundaries.end.x

This is what I do in my level code:

class_name MapLevel extends MapLevel2D

@onready var game_camera_2d: CharacterBoundCamera2D = %GameCamera2D
@onready var platforms: TileMapLayer = %Platforms


func _ready() -> void:
	super()
	game_camera_2d.change_camera_boundaries(platforms)

I have a few constructive comments on your code, take them or leave them. Some are based on the Godot Style Guide. these guidelines make your code easier to read if anyone else is reading your code, like say you’re asking for help on a forum. Others are just recommended programming practices.

  1. Signal names should be snake_case, not PascalCase.
  2. Function names should be snake_case, not PascalCase.
  3. pass should never be in production code. It is only used to stub out a function that has no code so that you do not get compiler warnings.
  4. Private functions (ones not used outside your current class) should always start with an underscore (_), like _update_limits()
  5. In a function, avoid unnecessary casting as it is time-consuming. (More on this below.)
  6. When possible, use a clear data type when passing an argument. (See below.)
  7. Try to keep your functions atomic. (See below.)

So let’s talk more about 4, 5 and 6. Your function UpdateLimits() takes an Array of Vector2s. It’s pretty clear from your code that you expect that Array to contain exactly two Vector2s. However, you only check to make sure the Array is not empty. You do not check to see if it only contains one Vector, or more than two. So you could get bad data. (Admittedly unlikely.) Regardless, a future user (which is 95% of the time you) of your function is going to have to go read your definition of your UpdateLimits() function to know how to use it.

The second issue is that you are passing arrays of Vector2 instead of Vector2i. So now, you have to convert those values in your function instead of just using them. Not the end of the world, BUT your code would be cleaner if your function assumed that a function calling it would clean up their data rather than you cleaning it up for them. Furthermore, I happen to know that TileMapLayers store the information you want as integers. So even if you don’t realize you are doing it, you are wasting cycles converting all that data to floats, passing it, then converting it back to ints.

There are a few ways you can handle this. One is to create your own class called something like MapBoundary that contains this data and pass that.

class_name MapBoundary extends Resource

var top_left_bounds: Vector2i
var bottom_right_bounds: Vector2i

Then your function signature would look something like:

func _update_limits(bounds : MapBoundary):

Now as long as they pass you a MapBoundary, they have to know what’s in it, and you don’t have to check it’s valid (if your Resource checks for you). It means instead of reading your function code, they just have to create that object.

Second option is to look to see if the language you’re using has a primitive or object you can use that would server your purpose. Well, it turns out that Godot has a definition of a rectangle, and specifically one for integers called a Rect2i. So now we can write our signature as:

func _update_limits(bounds : Rect2i):

It’s pretty easy to parse and create these. In fact, it’s really easy with a TileMapLayer because there’s a built-in function called get_used_rect(). So your _ready() function can change:

func _ready() -> void:
	LevelManager.tile_map_bounds_changed.connect(_update_limits)
	_update_limits(LevelManager.current_tilemap.get_used_rect())

You’ll also have to change LevelManager which now no longer needs a current_tilemap_bounds variable or the code to create it, and needs a change to the tile_map_bounds_changed signal to pass in the Rect2i by calling TileMapLayer.get_used_rect() itself.

This is where we get to talking about making your code atomic. Currently, your LevelManager code needs to understand how your PlayerCamera code works to pass it the right value. Even though we are no just passing a Rect2i, your LevelManager needs to know where that Rect2i comes from and how it’s constructed. Sure, in this case it’s not that complicated, but why does LevelManager need to know at all? It doesn’t.

We can simplify this even further by just passing the TileMapLayer itself. The obvious objection to that is that PlayerCamera doesn’t need all that information. It’s a waste of processing time and memory copying that whole thing over. Except Godot doesn’t do that. Godot passes variable by Reference what this means is that it passes the address in memory where your TileMapLayer is stored, which is a very small memory address. So the fact that you are only calling get_used_rect() on it doesn’t matter. It’s no more expensive than your LevelManager doing it.

More importantly you can call LevelManager doesn’t need to know how PlayerCamera works. It just needs to know to pass the TileMapLayer. Putting all that together, we get this:

class_name PlayerCamera extends Camera2D


func _ready() -> void:
	LevelManager.tile_map_bounds_changed.connect(_update_limits)
	_update_limits(LevelManager.current_tilemap)


func _update_limits(tile_map_layer: TileMapLayer):
	var map_boundaries: Rect2i = tile_map_layer.get_used_rect()
	var tile_size: Vector2i = tile_map_layer.tile_set.tile_size
	map_boundaries.position *= tile_size
	map_boundaries.size *= tile_size
	limit_top = map_boundaries.position.y
	limit_left = map_boundaries.position.x
	limit_bottom = map_boundaries.end.y
	limit_right = map_boundaries.end.x

	print(map_boundaries)
1 Like

Okay, wow! That’s a lot of great advice. I’ve gone through my code and adjusted my syntax to match the Godot Style Guide a little more like you said.

One of the things I’ve struggled most with is getting my scenes to transition. Right now I’ve implemented your code into mine but I’m getting errors when my game starts because my TileMapLayer hasn’t loaded yet, but I’m trying to access it in my _ready() function in the camera script. This is giving me this error:
Cannot call method 'get_used_rect' on a null value.
This error appears on this line:
var map_boundaries: Rect2i = tile_map_layer.get_used_rect()

Also, this is my current LevelManager script:

extends Node

signal level_load_started
signal level_loaded
signal tilemap_bounds_changed( bounds : Array[Vector2])

var current_tilemap : TileMapLayer
var target_transition : String
var position_offset : Vector2


func _ready():
	await get_tree().process_frame
	level_loaded.emit()
	if current_tilemap:
		return



func change_tilemap_bounds(bounds : Rect2i, tilemap: TileMapLayer):
	tilemap_bounds_changed.emit( bounds )
	current_tilemap = tilemap

	
func load_new_level(
...
)
...
	

I have some code in my load_new_level() script but that’s for loading new scenes.

As well as my current LevelTileMap code:

class_name LevelTileMap extends TileMapLayer


# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	LevelManager.tilemap = self
	LevelManager.change_tilemap_bounds(self.get_used_rect(), self)

I sincerely appreciate your help and comments.

Glad all that was helpful.

That’s a common problem. It’s called a race condition. The easiest way to deal with this is to have the TileMapLayer let the PlayerCamera know when it is ready. You actually already have enabled that with your LevelManager.tile_map_bounds_changed signal. So you may just need one small code changes.

In the PlayerCamera script, remove the initialization of bounds. in _ready():

func _ready() -> void:
	LevelManager.tile_map_bounds_changed.connect(_update_limits)

Since you’re already calling it from the LevelTileMap in its _ready() script, that should be enough. If not, this should solve your problem:

class_name LevelTileMap extends TileMapLayer


# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	LevelManager.tilemap = self
	ready.connect(_on_ready)


# This gets run once the entire object is fully constructed.
func _on_ready() -> void:
	LevelManager.change_tilemap_bounds(self.get_used_rect(), self)