How to center the player/camera in the middle of the window/screen , to mimic a 2.5d camera, so it looks like this game:

enter image description here

where I can move vertically and horizontally , and the camera still be centered on the player?

Edit: I want to center it 18 pixels in the 4 directions


Follow position

What first comes to mind is to use a RemoteTransform. You can set its remote path is the player avatar, and to only update the position (not the rotation, not the scale). Then make the Camera a child of the RemoteTransform and give it is rotation and offset position.

I suggest to use the split view (click View on the top of the editor and select Two Viewports) and then use one to preview the camera and the other to move it.

Follow x and z

Sadly RemoteTrasnform does not gives you the granularity to say that you want to follow the x and z but not the y. However we can mimic it:

extends Spatial

export var remote_path:NodePath
export var y:float

func _process(_delta: float) -> void:
    var target = get_node_or_null(remote_path) as Spatial
    if !is_instance_valid(target):
        return # either null or freed

    var target_position = target.global_transform.origin
    var position = Vector3(target_position.x, y, target_position.z)
    global_transform = Transform.IDENTITY.translated(position)

func _get_configuration_warning() -> String:
    var target = get_node_or_null(remote_path) as Spatial
    if is_instance_valid(target):
        return ""
    return "The \"Remote Path\" property must point to a valid Spatial or Spatial-derived node to work."

Set the Remote Path to the player avatar. And, of course, you make the Camera a child of that node, and rotate and offset it appropriately.

Isometric view

Advice if you want the isometric view: The rotation you want 45 degrees (or 135, 225, 315) on y, and -35.264 on x. Set projection to Orthogonal. You may have to adjust the Size of the camera which controls how much you see in the Orthogonal projection.


This took some figuring out. I'll go over the script part by part explaining what is going on.

tool extends Spatial

It is Spatial because 3D.

The reason why it is a tool script is to give those warnings that appear on the scene tree. We do that by implementing _get_configuration_warning. Let us get _get_configuration_warning out of the way:

func _get_configuration_warning() -> String:
    var target = get_node_or_null(remote_path) as Spatial
    if !is_instance_valid(target):
    return ""

These are the variables we will export:

export var remote_path:NodePath
export var y:int
export var margin:Vector2 = Vector2.ZERO

The variable remote_path is the NodePath to what the camera should follow. The variable y is the value for y we use in the transform (because we only take x and z). And the variable margin is how far off center on the screen we allow what we are following to go before we begin moving.

I have gone back and forth between whether or not we should use _process or _physics_process. On one hand the player avatar will only update on _physics_process. On the other this is about something visual (the Camera). At the end I ended up opting for _process because what this node follows might not necessarily be a physics body, and even if it is, it could be using _process for interpolation.

Since this is a tool script, _process begins like this:

func _process(_delta: float) -> void:
    if Engine.editor_hint:

Similarly we don't want it to run when it isn't in the scene tree:

    if !is_inside_tree():

Now let us get a reference to the viewport:

    var viewport:Viewport = get_viewport()

Just in case, let us check it is valid:

    if !is_instance_valid(viewport):

Now we use it to get the current camera:

    var camera:Camera = viewport.get_camera()

We not only need to check if it is valid, we also need to check if it is the child of this node.

    if !is_instance_valid(camera) or camera.get_parent() != self:

We also need the the target that the camera should follow:

    var target = get_node_or_null(remote_path) as Spatial
    if !is_instance_valid(target):

And we are going to need the coordinates of the center of the screen:

    var viewport_center := viewport.size * 0.5

Alright, we get the position we want to follow:

    var target_position := target.global_transform.origin

We only want x and z, of course:

    var position_3d := Vector3(target_position.x, y, target_position.z)

We figure out where on the screen that is:

    var position_2d := camera.unproject_position(position_3d)

We figure out the difference from the center of the screen:

    var off_center := position_2d - viewport_center

And we clamp it to the margin:

    var off_center_clamped := Vector2(
            clamp(off_center.x, -margin.x, margin.x),
            clamp(off_center.y, -margin.y, margin.y)

If clamping did nothing, then we don't need to move:

    if off_center == off_center_clamped:

Now, a tricky bit:

    var target_2d := viewport_center + off_center - off_center_clamped

This is the position in 2D that we want to put on the center. Notice that when off_center and off_center_clamped are equal, then target_2d is viewport_center. Meaning we want at the center, what is at the center.

However, off_center and off_center_clamped are different. So target_2d will be moved offset from the center according to that difference.

And now another tricky bit:

    var ray_origin := camera.project_ray_origin(target_2d)
    var ray_direction := camera.project_ray_normal(target_2d)

That's right, we are ray casting. We need to convert that position back to something in 3D.

    var depth := (y - ray_origin.y) / ray_direction.y

So what's that?

Let us see. We want to track a position on an horizontal plane defined by y. So we need to intersect the ray with that plane.

So we have some equation that looks like this:

(ray_origin + ray_direction * depth).y = y

Let us put some vector there:

ray_origin + ray_direction * depth = _target_3d
whatever.y = y

Solve for depth:

ray_origin + ray_direction * depth = _target_3d
ray_direction * depth = _target_3d - ray_origin
depth = (_target_3d - ray_origin) / ray_direction

And we only care about the y, so:

depth = (whatever.y - ray_origin.y) / ray_direction.y


    var depth := (y - ray_origin.y) / ray_direction.y

Alright, we can now figure out that point. Just plug that equation again:

    var target_3d := ray_origin + ray_direction * depth

Which, by the way, is the same as:

    var target_3d := camera.project_position(_target_2d, depth)

Except we already did all the work anyway.

Finally, set the transform:

global_transform = Transform.IDENTITY.translated(target_3d)

Usage is as expected. Make this node the parent of the Camera, set its properties. Position the camera.

  • \$\begingroup\$ is there a way to limit/center the camera , so it's 18 pixels in the 4 directions, so from the center/player to the right edge is 18 , and so on for the other directions ? \$\endgroup\$
    – Abanoub
    May 22 at 16:55
  • \$\begingroup\$ @Abanoub answer extended. \$\endgroup\$
    – Theraot
    May 22 at 21:10
  • \$\begingroup\$ thank you very much 🙏🏻 for this detailed answer, but can you please provide a demo project on github or something, because the camera configuration isn't working, also the fps got drop down to 15 fps, when switched to orthogonal, a demo would make me compare and match, thanks again \$\endgroup\$
    – Abanoub
    May 23 at 6:55
  • \$\begingroup\$ @Abanoub github.com/AbanoubNassem/game/pull/1 \$\endgroup\$
    – Theraot
    May 23 at 8:37
  • \$\begingroup\$ thank you very much , but the player is off a bit to the right, I believe we need to subtract the width of it , so it's 100% centered ? \$\endgroup\$
    – Abanoub
    May 23 at 10:04

Your Answer

