Search code examples
mathquaternionsgame-developmenteuler-anglesgodot

Move and rotate object around pivot and resume from where it stopped


I'd like to move an object around another - just as if the one object was a child of the other. This is GDscript - Godot Engine 3.2, but the logic should be very similar to other game engines.

Whenever I hold spacebar the green cube follows the blue cubes rotation.

First GIF demonstrates the green cube starting at position Vector3(0, 4, 0) without any rotation applied. This works perfectly fine.

In the second GIF I'm holding and releasing spacebar repeatedly. I would expect the green cube to continue from where it left, but instead it "jumps" to a new position and continues from there.

enter image description here

enter image description here

Code below does not include the actual rotation of the blue cube (pivot point), but only the calculations needed to move/rotate the green cube. Rotation of the blue cube is not an issue. Also, rotation is just for the sake of demonstration - in real scenario the blue cube would be moving around as well.

Rotations are calculated using quaternion, but this is not a requirement.

extends Node

var _parent
var _child
var _subject
var _positionOffset: Vector3
var _rotationOffset: Quat

func _ready():
    _parent = get_parent()
    _child = $"/root/Main/Child"

func _input(event):
    if event is InputEventKey:
        if event.scancode == KEY_SPACE:
            if event.is_action_pressed("ui_accept") and  _child != null:
                _subject = _child
                _set_subject_offset(_parent.transform, _child.transform)
            elif event.is_action_released("ui_accept"):
                _subject = null

func _set_subject_offset(pivot: Transform, subject: Transform):
    _positionOffset = (pivot.origin - subject.origin) 
    _rotationOffset = pivot.basis.get_rotation_quat().inverse() * subject.basis.get_rotation_quat()

func _rotate_around_pivot(subject_position: Vector3, pivot: Vector3, subject_rotation: Quat):
    return pivot + (subject_rotation * (subject_position - pivot))

func _physics_process(_delta):
    if _subject == null: return

    var target_position = _parent.transform.origin - _positionOffset
    var target_rotation = _parent.transform.basis.get_rotation_quat() * _rotationOffset
    _subject.transform.origin = _rotate_around_pivot(target_position, _parent.transform.origin, target_rotation) 
    _subject.set_rotation(target_rotation.get_euler())

I feel like I'm missing something obvious.


Solution

  • You can easily achieve this by temporarily parenting the subject to the pivot point while preserving the global transform:

    extends Spatial
    
    onready var obj1: Spatial = $Object1
    onready var obj2: Spatial = $Object2
    
    func reparent(obj: Spatial, new_parent: Spatial):
        # Preserve the global transform while reparenting
        var old_trans := obj.global_transform
        obj.get_parent().remove_child(obj)
        new_parent.add_child(obj)
        obj.global_transform = old_trans
    
    func _physics_process(delta: float):
        obj1.rotate_z(delta)
    
    func _input(event: InputEvent):
        if event.is_action_pressed("ui_accept"):
            reparent(obj2, obj1)
        elif event.is_action_released("ui_accept"):
            reparent(obj2, self)
    

    enter image description here

    If reparenting isn't feasible, you could instead parent a RemoteTransform to the pivot, and have that push its transform to the object you want to rotate:

    extends Spatial
    
    onready var remote_trans: RemoteTransform = $RemoteTransform
    
    func _process(delta):
        rotate_z(delta)
    
    func attach(n: Node):
        # move the remote so the target maintains its transform
        remote_trans.global_transform = n.global_transform
        remote_trans.remote_path = n.get_path()
    
    func detach():
        remote_trans.remote_path = ""