jrnl · home about list

# box2d - making b2Body clonable or copyable

I've been porting LunarLander from Openai/gym (Python) to C++. Since I'm using MCTS and related planners, the environment needs to be copyable. Well, box2d does not have copy-constructors, so at first glance we're screwed. Fortunately the environment is rather simple - it has three dynamic bodies (lander and the two legs), two joints and a static ground, the moon. It turns out, with a minor modification to box2d itself and a small hack that fixes sleeping it is possible to copy the whole LunarLander world.

Now, to clone the world and its bodies, I use the reset() method of the environment, i.e. we start with a clean initial world where the lander is still at its initial position. The shape of the moon can be copied easily, since it is generated from a random float vector. Simply copy the vector and generate the exact same moon. Finally, each body is cloned with the following function:

void LunarLanderEnv::clone_body(b2Body* current, b2Body* other) {
  auto transform = other->GetTransform();
  current->SetAwake(other->IsAwake());
  current->SetAngularVelocity(other->GetAngularVelocity());
  current->SetLinearDamping(other->GetLinearDamping());
  current->SetAngularDamping(other->GetAngularDamping());
  current->SetGravityScale(other->GetGravityScale());
  current->SetEnabled(other->IsEnabled());

  auto vel = other->GetLinearVelocity();
  current->SetLinearVelocity(vel);
  current->SetTransform(transform.p, transform.q.GetAngle());
  current->SetSleepTime(other->GetSleepTime());
}

Basically, all properties like position (transform), angle and velocity are copied. These are all public functions of b2Body except for GetSleepTime and SetSleepTime, which I had to add myself. The change to box2d itself can be found here.

The gist is - when setting the velocity, sleep time is discarded (set to 0.0f). That is very bad, since the environment ends when the lander is asleep (IsAwake() returns false, i.e. sleep time exceeds some threshold). Thus, sleep time needs to be copied too.

inline float b2Body::GetSleepTime() const
{
    return m_sleepTime;;
}

inline void b2Body::SetSleepTime(float newSleepTime) {
  m_sleepTime = newSleepTime;
}

It's unfortunate that the sleep time is not public, but it makes sense - we are now going rather deep into box2d. They do not support cloning/copying bodies, it is what it is. But for my purposes this works.

Lastly, the hack I unfortunately had to introduce (and I hope at some point I can get rid of it), is as follows.

clone_body(leg1, other.leg1);
clone_body(leg2, other.leg2);
clone_body(lander, other.lander);

for (int i = 0; i < 2; ++i) {
  world->Step(1.0 / FPS, 6 * 30, 2 * 30);
  world->Step(1.0 / FPS, 6 * 30, 2 * 30);
  world->Step(1.0 / FPS, 6 * 30, 2 * 30);
  world->Step(1.0 / FPS, 6 * 30, 2 * 30);
  world->Step(1.0 / FPS, 6 * 30, 2 * 30);
  clone_body(leg1, other.leg1);
  clone_body(leg2, other.leg2);
  clone_body(lander, other.lander);
}

So, of course you can do the 5 world->Step calls in a for loop. But the gist is, we need to give the bodies a chance to settle down and fall asleep. It seems when forcefully copying all the public properties of bodies (velocity etc.) the bodies still jiggle around. Maybe it's due to the joints, I don't know. Apparently there is still some state I missed copying. The hack makes it all work for now, but maybe at some point I will be able to solve this in a cleaner way.

The project this is relevant to is GRAB0. The full implementation of the LunarLander environment can be found here. This includes the above code snippets in a wider context.

A related post is the Torch C++ tutorial with a few more code snippets.

Published on