Search code examples
game-developmentgodotgdscript

How to instance a scene and have it face only an x or z direction?


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.


Solution

  • 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.