Consistently keeping the area that lets the Camera shifts screens in the same location every screen?

Godot Version

4.4 Stable

Question

Hello, I’m new here and I have been working on a Top-Down Shooter in the style of games such as Smash TV and Total Carnage in Godot 4.4. The game uses the resolution of 640x360 with 16x16 tiles. I have been making a system where the player camera snaps to a room based on if the player leaves the designated resolution (640x360). 16x16 tiles don’t perfectly fit onto the screen which led me to adjust the camera where 4 pixels are cut off from the top and bottom to perfectly align the tiles to match.

In the script for the camera which is a child of the player node, I have the process function and an update_screen function that controls the camera view port and when the camera should shift to the next screen based on the player’s location. Some rooms need to have a free moving camera with limits so the cam_lock if statement needs to stay.

What I have noticed is that the further vertically the player travels, the more de-synced and off center the camera shift point is from the edge of the screen. It should be that the middle of the player touching the edge of the screen is what switches the camera to the next screen. Is there something I could change about my equations so the edge of the screen will align with when the camera shifts? What I did for the camera global_position in the update_screen function is an example of what I would be talking about.

In the video example below you can see how the area where the camera should be shifting moves further up the y-axis as the player continues. It should be aligned with the edge of the screen. I know this is happening due to the screen_size = (640,360). Is there a way to adjust the point at which the camera shifts to same coordinates where the edge of the screen would be?

Thank you all very much!

var screen_size = 0
var cur_screen := Vector2(0, 0)
var parent_screen := Vector2(0, 0)
@export var cam_lock = true;

func _ready():
    screen_size = Vector2(640,360)
    set_as_top_level(true)
    global_position = get_parent().global_position

func _process(_delta) -> void:
    if (cam_lock != false):
        parent_screen=(get_parent().global_position/(Vector2(screen_size.x,screen_size.y))).floor()
        #checks if the current screen is equal to the screen where the player is located.
        if not parent_screen.is_equal_approx(cur_screen):
        	_update_screen(parent_screen)
    else:
        set_position(TargetNode.get_position().round())
        global_position = global_position.round()

#Updates what screen the camera is locked to.
func _update_screen(new_screen : Vector2):
    cur_screen = new_screen
    global_position = Vector2(cur_screen.x,cur_screen.y)*screen_size+screen_size*0.5
    global_position.y -= 4*(cur_screen.y+(cur_screen.y-1))

Video Example

Do you have the debug tools embedded in the window? If so you screen isn’t actually 360 pixels high. Turn off the embedded window on the Game tab and your code should start working.

image

Just click the three dots at the end and make sure Embed Game on Next Play is off and re-run the game.

image

I have made the change you have suggested but the behaviors of my code are still the same with the point where the camera should shift being off center.

Well you’ve got a lot of stuff going on in your code, and a lot of magic numbers. My recommendation for debugging would be to print out the math results every step of the way. You’ll find your problem that way.

This may be your problematic line, because you’re saying if it’s close enough, it’s true.

        if not parent_screen.is_equal_approx(cur_screen):
        	_update_screen(parent_screen)

You’d be better off doing:

        if parent_screen != cur_screen:
        	_update_screen(parent_screen)

You’re also rounding numbers, which is going to throw your math off:

        set_position(TargetNode.get_position().round())
        global_position = global_position.round()

I also want to take a quick detour on this line:

if (cam_lock != false):

This is the same as:

if cam_lock != false:

Which is easier to read without the negative:

if cam_lock == true:

Which can be reduced to:

if cam_lock:

Honestly though, I’d recommend you rewrite your code completely. Your _update_screen() function has an argument called new_screen that isn’t actually describing a screen. It describes a point, from which you then assume the screen size with magic numbers. Likely, your issue is that code. However you could fix it pretty easily just by making it a function that doesn’t assume screen size and works for any resolution - and that uses existing camera functions.

func _update_screen(new_screen: Rect2i) -> void:
	limit_top = new_screen.position.y
	limit_left = new_screen.position.x
	limit_bottom = new_screen.end.y
	limit_right = new_screen.end.x

Now you can determine the dimensions and send that to the function. If you later want to change the screen size, or want a level that is larger than one screen before scrolling, you don’t need new code.

Note that the variable cur_screen is no longer referenced, because you don’t need it. The Camera2D’s own limit_* variables handle that.


Then you’ve got the way you detect if you’re close to the edge. You are again relying on a certain screen size, which requires magic numbers (this time a magic vector). Instead, you could define an edge size, and if the player is within that edge, transition.

const EDGE = 10 #pixels

@export var cam_lock := true #note the lack of a semicolon
@export var screen_size := Vector2i(640, 360)
@export var starting_screen_position := Vector2i.ZERO

var screen: Rect2i

func _ready() -> void:
	screen.position = starting_screen_position
	screen.end = starting_screen_position + screen_size
	set_as_top_level(true)
	# global_position shouldn't need to be set here, as it's always the same as the parent unless you changed it.

func _process(_delta: float) -> void:
	if cam_lock:
		# Move Up
		if global_position.y < screen.position.y + EDGE:
			screen.position.y -= screen_size.y
			_update_screen(screen)
		# Move Down
		elif global_position.y > screen.end.y - EDGE:
			screen.position.y += screen_size.y
			_update_screen(screen)
		# Move Left
		if global_position.x < screen.position.x + EDGE:
			screen.position.x -= screen_size.x
			_update_screen(screen)
		# Move Right
		elif global_position.x > screen.end.x - EDGE:
			screen.position.x += screen_size.x
			_update_screen(screen)
    else: #No idea what you're doing here so I left it alone
        set_position(TargetNode.get_position().round())
        global_position = global_position.round()


func _update_screen(new_screen: Rect2i) -> void:
	limit_top = new_screen.position.y
	limit_left = new_screen.position.x
	limit_bottom = new_screen.end.y
	limit_right = new_screen.end.x

Now all you need is to know where the coordinates of the upper left-hand corner of your first screen starts and set it as the exported starting_screen_position variable.

Personally, I would get rid of the _process() function altogether, put four Area2Ds around the perimeter of the screen (not the level) and just paste the _process() code to an _on_area_entered() function that is tied to all four Area2Ds. Then you’re not running it every frame.

Plugging the code you have presented in, I get this result, I’ll have to figure out why camera code is messing with the player’s sprites as it should only change frames based on movement and cursor position. I will mess with this more in the next few days and see how I can implement these concepts and get back to you If I run into more issues. The 4 area nodes for the screen isn’t a bad idea but I have bigger rooms where the camera allows scrolling and the doors may be in different locations based on the room shape which wouldn’t work with that system. Thank you for the nudge in the right direction though!

Video

An alternative to calculating each new camera location is to store the actual values in a const array and use an index to retrieve them as necessary:

enum {NORTH, SOUTH, EAST, WEST}
const CAMERA_POSITIONS:Array[Vector2] = [
   Vector2(0,0), Vector2(0, 361), Vector2(0,421)...   
]  
var cam_pos_index = 0  
func _update_screen(entry_dir:String)->void: 
   match entry_dir: 
      NORTH:  
         cam_pos_index += 1
      SOUTH:  
         cam_pos_index -= 1
      EAST:
         cam_pos_index += NUMBER_OF_SCREENS
      WEST:
         cam_pos_index -= NUMBER_OF_SCREENS
   global_position = CAMERA_POSITIONS[cam_pos_index]  
...etc

Obviously this mock up code would have to be flushed out to make it work. It is shown just to give an idea what I am thinking.

So would I type out every point where the camera should shift manually into an array and then have that index called based on the current screen of the player? Example being: if player moved to screen Vector2(4,3) make camera position = position found in index array[[4],[3]]?