libgdxbox2d

Revoluting balls on a moving body


I'm using Android Studio with Java and LibGDX.

I need to make some balls revoluting on another moving body.

It works if the body doesn't move:

enter image description here

Otherwise the balls are "pulled" by the moving body instead of revoluting on it.

These are some screenshots while the body is moving to the bottom:

enter image description here

I noticed that if I heavy increase the revoluting speed, the balls revolute almost correctly but I need to make it works with many different speeds.

Either I move the main ball with setTransform or with linearVelocity the result is the same.

How to maintain a smooth revoluting movement while the main ball is moving?

Thanks


public Ball(World world, float x, float y, float sizeInWorld) {
    this.sizeInWorld = sizeInWorld;
    this.ballInitialSize = sizeInWorld;
    
    BodyDef bodyDef = new BodyDef();
    bodyDef.type = BodyDef.BodyType.KinematicBody;
    bodyDef.position.set(x, y);
    bodyDef.fixedRotation = false;
    bodyDef.bullet = true;
    body = world.createBody(bodyDef);
    body.setLinearDamping(0);

    CircleShape circle = new CircleShape();
    circle.setRadius(sizeInWorld / 2);
    FixtureDef fixtureDef = new FixtureDef();
    fixtureDef.shape = circle;
    fixtureDef.friction = 0f;
    fixtureDef.restitution = 1f;
    fixtureDef.density = 1f;
    circle.dispose();
    
    body.setLinearVelocity(0, -10);
}

public BallRevoluting(World world, float x, float y, float sizeInWorld) {
    this.sizeInWorld = sizeInWorld;
    this.ballInitialSize = sizeInWorld;
    
    BodyDef bodyDef = new BodyDef();
    bodyDef.type = BodyDef.BodyType.DynamicBody;
    bodyDef.position.set(x, y);
    bodyDef.fixedRotation = false;
    bodyDef.awake = true;
    bodyDef.bullet = true;
    body = world.createBody(bodyDef);
    body.setLinearDamping(0);

    CircleShape circle = new CircleShape();
    circle.setRadius(sizeInWorld / 2);
    FixtureDef fixtureDef = new FixtureDef();
    fixtureDef.shape = circle;
    fixtureDef.friction = 0.1f;
    fixtureDef.isSensor = true;
    fixtureDef.restitution = 0;
    fixtureDef.density = 1f;
    circle.dispose();
}

public static void createRevoluteJoint(World world, Body body, Body revolutingBody, float speed, boolean clockWise) {
    RevoluteJointDef revoluteJointDef = new RevoluteJointDef();
    revoluteJointDef.initialize(body, revolutingBody, body.getPosition());
    revoluteJointDef.enableMotor = true;
    float targetVelocity = speed * 5;
    float radius = revolutingBody.getPosition().dst(body.getPosition());
    revoluteJointDef.maxMotorTorque = calculateMotorTorque(revolutingBody, targetVelocity, radius);
    revoluteJointDef.motorSpeed = targetVelocity / radius;
    if(clockWise) {
        Gdx.app.log(TAG, "using revoluteToClockwise direction");
        revoluteJointDef.motorSpeed = -revoluteJointDef.motorSpeed;
    }
    world.createJoint(revoluteJointDef);
}

public calculateMotorTorque(Body body, float targetVelocity, float radius) {
    float mass = body.getMass();
    float angularVelocity = targetVelocity / radius;
    float inertia = mass * radius * radius;
    return inertia * angularVelocity;
}


public Ball mainBall = new Ball(world, 0, 0, 1);

int numOrbitingBalls = 4;
float orbitRadius = 2.0f;
for (int i = 0; i < numOrbitingBalls; i++) {
    float angle = (float) (i * 2 * Math.PI / numOrbitingBalls);
    float x = mainBall.getPosition().x + orbitRadius * (float)Math.cos(angle);
    float y = mainBall.getPosition().y + orbitRadius * (float)Math.sin(angle);

    BallRevoluting newBall = new BallRevoluting(world, x, y, mainBall.sizeInWorld/2);

    createRevoluteJoint(world, mainBall.body, newBall.body, 1, true);
}

Solution

  • Consider using a single Body with the satellite-balls as Fixtures on that Body.

    If that doesn't work for your use-case, one approach is to use WeldJoints instead: enter image description here

    In the example above I enable movement first and then rotation of the mainBall, but it could have been done in one step.

    If this approach doesn't work either (let's say because your 'mainBall' is not allowed to have a rotation), then I would create a body with 4 fixtures for the satellites, and then attach that body to the mainBall with a MotorJoint with offset zero.

    Full source for the gif:

    import com.badlogic.gdx.ApplicationAdapter;
    import com.badlogic.gdx.Gdx;
    import com.badlogic.gdx.Input;
    import com.badlogic.gdx.graphics.GL20;
    import com.badlogic.gdx.graphics.OrthographicCamera;
    import com.badlogic.gdx.graphics.Texture;
    import com.badlogic.gdx.graphics.g2d.SpriteBatch;
    import com.badlogic.gdx.math.Vector2;
    import com.badlogic.gdx.physics.box2d.*;
    import com.badlogic.gdx.physics.box2d.joints.DistanceJointDef;
    import com.badlogic.gdx.physics.box2d.joints.RevoluteJointDef;
    import com.badlogic.gdx.physics.box2d.joints.WeldJointDef;
    import com.badlogic.gdx.utils.ScreenUtils;
    
    public class SandboxGame extends ApplicationAdapter {
    
        public OrthographicCamera camera;
        public World world;
        public Box2DDebugRenderer box2DDebugRenderer;
        public Ball mainBall;
    
        @Override
        public void create() {
            float w = 32.0f;
            float h = w * Gdx.graphics.getHeight() / (float)Gdx.graphics.getWidth();
            camera = new OrthographicCamera(w, h);
            camera.position.set(0, 0, 1);
            camera.update();
            world = new World(Vector2.Zero, false);
            box2DDebugRenderer =new Box2DDebugRenderer();
            mainBall = new Ball(world, -10, 0, 1);
    
            int numOrbitingBalls = 4;
            float orbitRadius = 2.0f;
            for (int i = 0; i < numOrbitingBalls; i++) {
                float angle = (float) (i * 2 * Math.PI / numOrbitingBalls);
                float x = mainBall.body.getWorldCenter().x + orbitRadius * (float)Math.cos(angle);
                float y = mainBall.body.getWorldCenter().y + orbitRadius * (float)Math.sin(angle);
    
                BallRevoluting newBall = new BallRevoluting(world, x, y, mainBall.sizeInWorld/2);
                createWeldJoint(world, mainBall.body, newBall.body);
            }
        }
    
        @Override
        public void render() {
            if (Gdx.input.isKeyJustPressed(Input.Keys.ENTER))
                mainBall.body.setAngularVelocity(8);
    
            if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE))
                mainBall.body.setLinearVelocity(4, 0);
            camera.update();
            world.step(Gdx.graphics.getDeltaTime(), 8, 8);
            ScreenUtils.clear(0.15f, 0.15f, 0.2f, 1f);
            box2DDebugRenderer.render(world, camera.combined);
        }
    
        public static void createWeldJoint(World world, Body body, Body revolutingBody) {
            WeldJointDef weldJointDef = new WeldJointDef();
            weldJointDef.initialize(body, revolutingBody, body.getWorldCenter());
            world.createJoint(weldJointDef);
        }
    
        public static class Ball
        {
            private final float sizeInWorld;
            private final Body body;
    
            public Ball(World world, float x, float y, float sizeInWorld) {
                this.sizeInWorld = sizeInWorld;
                BodyDef bodyDef = new BodyDef();
                bodyDef.type = BodyDef.BodyType.KinematicBody;
                bodyDef.position.set(x, y);
                bodyDef.fixedRotation = false;
                bodyDef.bullet = true;
                body = world.createBody(bodyDef);
                body.setLinearDamping(0);
    
                CircleShape circle = new CircleShape();
                circle.setRadius(sizeInWorld / 2.0f);
                FixtureDef fixtureDef = new FixtureDef();
                fixtureDef.shape = circle;
                fixtureDef.friction = 0f;
                fixtureDef.restitution = 1f;
                fixtureDef.density = 1f;
                circle.dispose();
    
                body.createFixture(fixtureDef);
    
                //body.setLinearVelocity(0, -10);
            }
        }
    
    
        public static class BallRevoluting
        {
            private final Body body;
    
            public BallRevoluting(World world, float x, float y, float sizeInWorld) {
                BodyDef bodyDef = new BodyDef();
                bodyDef.type = BodyDef.BodyType.DynamicBody;
                bodyDef.position.set(x, y);
                bodyDef.fixedRotation = false;
                bodyDef.awake = true;
                bodyDef.bullet = true;
                body = world.createBody(bodyDef);
                body.setLinearDamping(0);
    
                CircleShape circle = new CircleShape();
                circle.setRadius(sizeInWorld / 2);
                FixtureDef fixtureDef = new FixtureDef();
                fixtureDef.shape = circle;
                fixtureDef.friction = 0.1f;
                fixtureDef.isSensor = true;
                fixtureDef.restitution = 0;
                fixtureDef.density = 1f;
                body.createFixture(fixtureDef);
                circle.dispose();
            }
        }
    }