psycotica0
psycotica0

Reputation: 3450

Is there an easier way to turn Canvas locations into Node2D locations

Ok, so I have a node tree in my main scene that is like this (simplified)

root
|- LevelHolder
|- Player
|- Camera2D
|- CanvasLayer
  |- SomeUI
    |- ...
  |- Extents
    |- TopLeft
    |- BottomRight

And the situation I have is that my Canvas Layer has a full rect layout to stretch across the screen, and then my SomeUI element contains a bunch of UI elements that make up a HUD and a frame. I've got a Camera2D that allows me to zoom in and out on my player and level, so the editor's placement of the Node2D elements doesn't match the in-game view, since the Camera moves and zooms.

I don't want my player to be able to move underneath my HUD elements, so I've got these Extents objects in the CanvasLayer, which I can anchor to the top-left and bottom-right corners of the part of my HUD that I actually want the player to be able to walk around in.

So what I think I need to do is turn Canvas positions into positions I can set for my player.

After a lot of trial and error, this is what worked for me:

var tl = $CanvasLayer/Extents/TopLeft
var tlgp = tl.get_viewport_transform() * tl.get_global_transform()
$Player.topleft = self.get_viewport_transform().affine_inverse().xform(tlgp.origin)

var br = $CanvasLayer/Extents/BottomRight
var brgp = br.get_viewport_transform() * br.get_global_transform()
$Player.bottomright = self.get_viewport_transform().affine_inverse().xform(brgp.origin)

Which works, but seems like a lot of work, and it took me a lot of screwing around to find this all. And I can leave out self.get_global_transform() in this case because I know that root's global transform is the identity, but in general it'd be even more complicated.

So, my real question is have I just overcomplicated this? Is there some method I could use that would just do all of this for me (or something else entirely) that would let me easily place a Node2D underneath a Canvas element, no matter what size the window is, or how it's stretched or squashed?

Upvotes: 4

Views: 2208

Answers (2)

idbrii
idbrii

Reputation: 11916

Here's a Godot 4 solution that you can put into canvas.gd and call CanvasF.world_pos_center(widget) from anywhere in your code:

class_name CanvasF


# Get world rect from a Control in a CanvasLayer.
#
# The control is positioned relative to the screen and this gets its rectangle
# in the world even as your Camera2D moves around the scene.
static func world_rect(control: Control) -> Rect2:
    var screen_coords = control.get_viewport_transform() * control.get_global_rect()
    var scene_root = control.get_tree().current_scene
    assert(
        scene_root as Node2D,
        "Cannot call from within a CanvasLayer scene. You must have a Node2D root."
    )
    var rect = scene_root.get_viewport_transform().affine_inverse() * screen_coords
    return rect


# Get world position of the centre of a control in a CanvasLayer.
static func world_pos_center(control: Control):
    return world_rect(control).get_center()

(The F in CanvasF stands for function and I use it to prevent clashes with future built-in Godot types.)

This solution builds on Theraot's answer, but works without passing a node (since I wanted to use it for DebugDraw2D). My key realization was that the two get_viewport_transform() calls return different results so I needed to find a suitable node in the 2D scene.

Upvotes: 0

Theraot
Theraot

Reputation: 40075

What you are doing is correct.

Is it overkill? Perhaps. Assuming the UI does not change in way that the extends need to move, you could have the screen coordinates computed beforehand… Or compute them only when necessary.

Another thing to consider is how you are moving the Camera2D. If the camera is following the player, you can use "drag margins" that you use to specify how close the the border of the screen it has to get before the view begins to move. Although that might not be viable depending on the game.

And for another alternative, you could put the game world in a Viewport, and perhaps use a ViewportContainer which will be part of the UI. Although Viewports are a performance consideration - in particular for mobile - chances are you can afford this solution. This approach also allows to have a different resolution for the game world and the UI. See also How to make a silky smooth camera for pixelart games in Godot

This rest of this answer is confirmation of what you are doing, and hopefully making it easier to understand.


The official documentation has a formula for the screen position:

var screen_coord = get_viewport_transform() * (get_global_transform() * local_pos)

See Viewport and canvas transforms.

First of all, we don't need to use get_global_transform() since we have property for it:

var screen_coord = get_viewport_transform() * (global_transform * local_pos)

If we know we are dealing with a Control and we want its position (i.e. Vector2.ZERO in local space) we can write a shorter version:

var screen_coord = get_viewport_transform() * rect_global_position

On the other hand, for a Node2D (and again we want its position) we can do this:

var screen_coord = get_viewport_transform() * global_position

Which is the same as:

var screen_coord = get_viewport_transform() * global_transform.origin

By the way, you get the same result like this (Which is what you are doing):

var screen_coord = (get_viewport_transform() * global_transform).origin

The difference is that here we are composing the transformation, which is extra work.


However, since we want to place a Node2D in the screen coordinates of something else, we need the inverse process.

Given this equation:

screen_coord = get_viewport_transform() * global_position

We can compose the inverse of one of these transforms at the start of both sides, like this:

get_viewport_transform().affine_inverse() * screen_coord
=
get_viewport_transform().affine_inverse() * get_viewport_transform() * global_position

And - of course - get_viewport_transform().affine_inverse() * get_viewport_transform() cancels out (to an identity transform, which we can omit), so we have:

get_viewport_transform().affine_inverse() * screen_coord = global_position

Flip it, and you have the means to position the Node2D at a given screen position:

global_position = get_viewport_transform().affine_inverse() * screen_coord

Now we can combine the computation of the screen position and how we position a Node2D at a given screen position. For example:

var screen_coords := control.get_viewport_transform() * control.rect_global_position
node2D.global_position = node2D.get_viewport_transform().affine_inverse() * screen_coords

You can use xform instead of * if you prefer. However, be aware that xform returns Variant, so you lose type information.

Upvotes: 0

Related Questions