3

I'm trying to create a "Block" input that summons a wall at the position the camera is looking at and have it face the camera's direction. It seems to be using the global coordinates which doesn't make sense to me, because I use the same code to spawn a bullet with no problem. Here's my code:

    if Input.is_action_just_pressed("light_attack"):
        var b = bullet.instance()
        muzzle.add_child(b)
        b.look_at(aimcast.get_collision_point(), Vector3.UP)
        b.shoot = true
        print(aimcast.get_collision_point())
    if Input.is_action_just_pressed("block"):
        var w = wall.instance()
        w.look_at(aimcast.get_collision_point(),Vector3.UP)
        muzzle.add_child(w)
        w.summon = true

The light attack input is the code used to summon and position the bullet. muzzle is the spawn location (just a spatial node at the end of the gun) and aimcast is a raycast from the center of the camera. All of this is run in a get_input() function. The wall spawns fine, I just can't orient it. I also need to prevent any rotation on the y-axis. This question is kinda hard to ask, so I couldn't google it. If you need any clarification please let me know.

Ken White
  • 123,280
  • 14
  • 225
  • 444

1 Answers1

2

New Answer

The asked comment made me realize there is a simpler way. In the old answer I was defining a xz_aim_transform, which could be done like this:

func xz_aim_transform(pos:Vector3, target:Vector3) -> Transform:
    var alt_target := Vector3(target.x, pos.y, target.z)
    return Transform.IDENTITY.translated(pos).looking_at(alt_target, Vector3.UP)

That is: make a fake target at the same y value, so that the rotation is always on the same plane.

It accomplishes the same thing as the approach in the old answer. However, it is shorter and easier to grasp. Regardless, I generalized the approach in the old answer, and the explanation still has value, so I'm keeping it.


Old Answer

If I understand correctly, you want something like look_at except it only works on the xz plane.

Before we do that, let us establish that look_at is equivalent to this:

func my_look_at(target:Vector3, up:Vector3):
    global_transform = global_transform.looking_at(target, up)

The take away of that is that it sets the global_transform. We don't need to delve any deeper in how look_at works. Instead, let us work on our new version.

We know that we want the xz plane. Sticking to that will make it simpler. And it also means we don't need/it makes no sense to keep the up vector. So, let us get rid of that.

func my_look_at(target:Vector3):
    # global_transform = ?
    pass

The plan is to create a new global transform, except it is rotated by the correct angle around the y axis. We will figure out the rotation later. For now, let us focus on the angle.

Figuring out the angle will be easy in 2D. Let us build some Vector2:

func my_look_at(target:Vector3):
    var position := global_transform.origin
    var position_2D := Vector2(position.x, position.z)
    var target_2D := Vector2(target.x, target.z)
    var angle:float # = ?
    # global_transform = ?

That part would not have been as easy with an arbitrary up vector.

Notice that we are using the 2D y for the 3D z values.

Now, we compute the angle:

func my_look_at(target:Vector3):
    var position := global_transform.origin
    var position_2D := Vector2(position.x, position.z)
    var target_2D := Vector2(target.x, target.z)
    var angle := (target_2D - position_2D).angle_to(Vector2(0.0, -1.0))
    # global_transform = ?

Since we are using the 2D y for the 3D z values, Vector2(0.0, -1.0) (which is the same as Vector2.UP, by the way) is representing Vector3(0.0, 0.0, -1.0) (Which is Vector3.FORWARD). So, we are computing the angle to the 3D forward vector, on the xz plane.

Now, to create the new global transform, we will first create a new basis from that rotation, and use it to create the transform:

func my_look_at(target:Vector3):
    var position := global_transform.origin
    var position_2D := Vector2(position.x, position.z)
    var target_2D := Vector2(target.x, target.z)
    var angle := (target_2D - position_2D).angle_to(Vector2.UP)
    var basis := Basis(Vector3.UP, angle)
    global_transform = Transform(basis, position)

You might wonder why we don't use global_transform.rotated, the reason is that using that multiple times would accumulate the rotation. It might be ok if you only call this once per object, but I rather do it right.

There is one caveat to the method above. We are losing any scaling. This is how we fix that:

func my_look_at(target:Vector3):
    var position := global_transform.origin
    var position_2D := Vector2(position.x, position.z)
    var target_2D := Vector2(target.x, target.z)
    var angle := (target_2D - position_2D).angle_to(Vector2.UP)
    var basis := Basis(Vector3.UP, angle).scaled(global_transform.basis.get_scale())
    global_transform = Transform(basis, position)

And there you go. That is a custom "look at" function that works on the xz plane.


Oh, and yes, as you have seen, your code works with global coordinates. In fact, get_collision_point is in global coordinates.

Thus, I advice not adding your projectiles as children. Remember that when the parent moves, the children move with it, because they are placed relative to it.

Instead give them the same global_transform, and then add them to the scene tree. If you add them to the scene before giving them their position, they might trigger a collision.

You could, for example, add them directly as children to the root (or have a node dedicated to holding projectiles, another common option is to add them to owner).

That way you are doing everything on global coordinates, and there should be no trouble.

Well, since you are going to set the global_transform anyway, how about this:

func xz_aim_transform(position:Vector3, target:Vector3) -> Transform:
    var position_2D := Vector2(position.x, position.z)
    var target_2D := Vector2(target.x, target.z)
    var angle := (target_2D - position_2D).angle_to(Vector2.UP)
    var basis := Basis(Vector3.UP, angle)
    return Transform(basis, position)

Then you can do this:

var x = whatever.instance()
var position := muzzle.global_transform.origin
var target := aimcast.get_collision_point()
x.global_transform = xz_aim_transform(position, target)
get_tree().get_root().add_child(x)
x.something = true
print(target)

By the way, this would be the counterpart of xz_aim_transform not constrained to the xz plane:

func aim_transform(position:Vector3, target:Vector3, up:Vector3) -> Transform:
    return Transform.IDENTITY.translated(position).looking_at(target, up)

It took me some ingenuity, but here is the version constrained to an arbitrary plane (kind of, as you can see it does not handle all cases):

func plane_aim_transform(position:Vector3, target:Vector3, normal:Vector3) -> Transform:
    normal = normal.normalized()
    var forward_on_plane := Vector3.FORWARD - Vector3.FORWARD.project(normal)
    if forward_on_plane.length() == 0:
        return Transform.IDENTITY

    var position_on_plane := position - position.project(normal)
    var target_on_plane := target - target.project(normal)
    var v := forward_on_plane.normalized()
    var u := v.rotated(normal, TAU/4.0)
    var forward_2D := Vector2(0.0, forward_on_plane.length())
    var position_2D := Vector2(position_on_plane.project(u).dot(u), position_on_plane.project(v).dot(v))
    var target_2D := Vector2(target_on_plane.project(u).dot(u), target_on_plane.project(v).dot(v))
    var angle := (target_2D - position_2D).angle_to(forward_2D)
    var basis := Basis(normal, angle)
    return Transform(basis, position)

Here w - w.project(normal) gives you a vector perpendicular to the normal. And w.project(u).dot(u) gives you how many times u fit in w, signed. So we use that build our 2D vectors.

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • Wow, what an excellent answer. Thank you so much. I totally forgot about Vector2. Couldn't I just get vector 2 positions, tweak the Vector2.y to be z and put 0 in for y, and put those values into the .look_at()? I'm confused about why Vector.UP is arbitrary. And about the adding projectile as a child, in the projectile's script, in the _ready() function I run set_as_toplevel(true) so that is intantly parentless. I'm gonna have to digest that answer as I still am relatively new to Godot, but nonetheless you did a fantastic job. I'd upvote 100 times if I could. – Spiro Cromwell Oct 18 '21 at 03:24
  • 1
    @SpiroCromwell Remember that you need a `Vector3` for `look_at`. However, now that you mention it, you could copy the `y` value over. I completely missed that: `x.gobal_transform = Transform.IDENTITY.translated(position).looking_at(Vector3(target.x, position.y, target.z), Vector.UP)`. Btw, `look_at` takes an `up` vector because there are infinite solutions otherwise. If you rotate the bullet around its forward line, it would still be looking at the same direction. The `up` vector is to remove that ambiguity. It never occurred to me to use `set_as_top_level` for bullets, but seems a good idea. – Theraot Oct 18 '21 at 03:58
  • Thanks for the clarification. I appreciate it! – Spiro Cromwell Oct 18 '21 at 13:08
  • Also I just tested it in game and it works perfectly! I would have never thought of using muzzle's global.transform.origin to get a y value. I had been stuck on this for a few days, I cannot thank you enough. – Spiro Cromwell Oct 18 '21 at 13:17