When you add a GrooveJoint2D to the scene tree, the Godot editor limits the minimum value of the initial_offset property to 1 px:

Godot editor - GrooveJoint2D

The documentation doesn't say anything about a minimum value of 1.

Setting the property to zero through code before assigning node_a and node_b seems to work though and doesn't exhibit any adverse effect for my use case.

$MyGrooveJoint.initial_offset = 0
$MyGrooveJoint.node_a = $MyNodeA.get_path()
$MyGrooveJoint.node_b = $MyNodeB.get_path()

Still there might be cases where an initial_offset of 0 could cause problems, which I'd like to know.


1 Answer 1


An initial_offset of 0.0 is OK.

The rest of this answer is how I confirmed that.

A good excuse to check the source code.

We find initial_offset declared like this (source):

real_t initial_offset = 25.0;

And looking for usage (in joint_2d.cpp) does not reveal any issue with it bean zero. However, it is used to compute a variable anchor_B which is passed to joint_make_groove in joint_make_groove (source):

void GrooveJoint2D::_configure_joint(RID p_joint, PhysicsBody2D *body_a, PhysicsBody2D *body_b) {
    Transform2D gt = get_global_transform();
    Vector2 groove_A1 = gt.get_origin();
    Vector2 groove_A2 = gt.xform(Vector2(0, length));
    Vector2 anchor_B = gt.xform(Vector2(0, initial_offset));

    PhysicsServer2D::get_singleton()->joint_make_groove(p_joint, groove_A1, groove_A2, anchor_B, body_a->get_rid(), body_b->get_rid());

As you can see, a value of zero in initial_offset results in anchor_B being set at the origin of the GrooveJoint2D.

We find joint_make_groove in godot_physics_server_2d.cpp (source):

void GodotPhysicsServer2D::joint_make_groove(RID p_joint, const Vector2 &p_a_groove1, const Vector2 &p_a_groove2, const Vector2 &p_b_anchor, RID p_body_a, RID p_body_b) {
    GodotBody2D *A = body_owner.get_or_null(p_body_a);

    GodotBody2D *B = body_owner.get_or_null(p_body_b);

    GodotJoint2D *prev_joint = joint_owner.get_or_null(p_joint);
    ERR_FAIL_COND(prev_joint == nullptr);

    GodotJoint2D *joint = memnew(GodotGrooveJoint2D(p_a_groove1, p_a_groove2, p_b_anchor, A, B));

    joint_owner.replace(p_joint, joint);

Which ends up passing the p_b_anchor to GodotGrooveJoint2D... Which we find in godot_joints_2d.h and godot_joints_2d.cpp. First of all, it will convert it to the local space of the second body in the constructor (source):

B_anchor = B->get_inv_transform().xform(p_b_anchor);

And then, in setup the computation of rB would be the zero vector (source):

    rB = B->get_transform().basis_xform(B_anchor);

    // calculate tangential distance along the axis of rB
    real_t td = (B->get_transform().get_origin() + rB).cross(n);
    // calculate clamping factor and rB
    if (td <= ta.cross(n)) {
        clamp = 1.0f;
        rA = ta - A->get_transform().get_origin();
    } else if (td >= tb.cross(n)) {
        clamp = -1.0f;
        rA = tb - A->get_transform().get_origin();
    } else {
        clamp = 0.0f;
        //joint->r1 = cpvsub(cpvadd(cpvmult(cpvperp(n), -td), cpvmult(n, d)), a->p);
        rA = ((-n.orthogonal() * -td) + n * d) - A->get_transform().get_origin();

Although, I believe that is no problem. Then we pass rB to k_tensor:

    // Calculate mass tensor
    k_tensor(A, B, rA, rB, &k1, &k2);

Which looks like this (source):

static inline void
k_tensor(GodotBody2D *a, GodotBody2D *b, Vector2 r1, Vector2 r2, Vector2 *k1, Vector2 *k2) {
    // calculate mass matrix
    // If I wasn't lazy and wrote a proper matrix class, this wouldn't be so gross...
    real_t k11, k12, k21, k22;
    real_t m_sum = a->get_inv_mass() + b->get_inv_mass();

    // start with I*m_sum
    k11 = m_sum;
    k12 = 0.0f;
    k21 = 0.0f;
    k22 = m_sum;

    r1 -= a->get_center_of_mass();
    r2 -= b->get_center_of_mass();

    // add the influence from r1
    real_t a_i_inv = a->get_inv_inertia();
    real_t r1xsq = r1.x * r1.x * a_i_inv;
    real_t r1ysq = r1.y * r1.y * a_i_inv;
    real_t r1nxy = -r1.x * r1.y * a_i_inv;
    k11 += r1ysq;
    k12 += r1nxy;
    k21 += r1nxy;
    k22 += r1xsq;

    // add the influnce from r2
    real_t b_i_inv = b->get_inv_inertia();
    real_t r2xsq = r2.x * r2.x * b_i_inv;
    real_t r2ysq = r2.y * r2.y * b_i_inv;
    real_t r2nxy = -r2.x * r2.y * b_i_inv;
    k11 += r2ysq;
    k12 += r2nxy;
    k21 += r2nxy;
    k22 += r2xsq;

    // invert
    real_t determinant = k11 * k22 - k12 * k21;
    ERR_FAIL_COND(determinant == 0.0);

    real_t det_inv = 1.0f / determinant;
    *k1 = Vector2(k22 * det_inv, -k12 * det_inv);
    *k2 = Vector2(-k21 * det_inv, k11 * det_inv);

We see that it gets the rB argument in the r2 parameter. We also see that r2 is offset by the center of mass of the second body. And finally that if this somehow resulted in an invalid matrix, we would have an error.

Thus, as long as you are not getting an error in the output of your code, it is fine.

  • \$\begingroup\$ Thanks for the deep dive into the code! So we should propably create an issue in the github project so the minium value that can be entered in the UI will be changed to 0.0? This would also further confirm whether 0.0 is a valid value. \$\endgroup\$
    – zett42
    Apr 15, 2023 at 19:27
  • \$\begingroup\$ @zett42 sure. I could not find an issue about it. \$\endgroup\$
    – Theraot
    Apr 15, 2023 at 19:52
  • \$\begingroup\$ I'm a little bit hesitant as I'm still very new to Godot. If it's not asked too much, would you like to create the issue? With the research you have already done, you can propably formulate it much better than me. \$\endgroup\$
    – zett42
    Apr 16, 2023 at 11:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .