1
\$\begingroup\$

I am trying to set the volume of a bouncing sound of a ball depending on how hard the ball hits something. so the harder the ball hits a wall or a ground the more loud the bounce sound.

I couldn't find anyway to get the impulse of a body other than the postSolve(Contact contact, ContactImpulse impulse) method inContactListner class.

But i then realized that unlike the beginContact(Contact contact) method, postSolve(Contact contact, ContactImpulse impulse) gets called repeatedly until the contact ends. so it repeatedly plays the bouncing sound making it sound stalked and buggy.

I cant use the beginContact(Contact contact) method because there is no way to get the impulse that the ball is receiving from this method. I tried to to use the velocity of the ball instead of impulse inside beginContact(Contact contact)but that didnt yield good results and found it very complicated as i had to consider both horizontal and vertical speeds.

so my question is how can I make the sound play only once if i am using the postSolve(Contact contact, ContactImpulse impulse) method or is there any other solution to achieve what i am trying to do here?

\$\endgroup\$
1
  • \$\begingroup\$ Presumably you considered caching some information each time you play a sound, then next time you go to play it, checking your cache to see if you've very recently played that sound in the same vicinity, and skipping playing a new sound if so? You'd need this even if postSolve were called only once, in cases where a bunch of small objects vibrate against one another over consecutive frames. \$\endgroup\$
    – DMGregory
    Sep 4 '20 at 11:01
1
\$\begingroup\$

Playing sounds from the beginContact, endContact, preSolve and postSolve methods is not a good idea (as you've identified) as the methods may be called many times per World step depending on the number of collisions that occurred.

What you can do in these methods is to collect information so that you can use it later when the Box2D World step is done.

One way to achieve what you're trying to do would be to

  1. Capture the collision event in postSolve and if the involved Body or Fixture is something that have special collision handling then notify the userObject that the collision has occured and with which force.
  2. In the userObject, store the information of all such notifications.
  3. In an update method that runs once per game-loop tick figure out if a sounds needs to be played and play it.
  4. Clear the stored information about collisions.

The above approach limits the number of sounds that is being requested to play to one per object per game-loop tick, rather than one per recorded collision.

If one sound per game-loop tick is too many sound you can easily add a counter for the number of currently playing sounds for each object and cap it.

I can't easily demonstrate it with actual audio here, but here's an example where I draw red circles with a radius that represents the volume of the sound:

enter image description here

The full source code for the above:

import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.*;

public class BouncingSounds extends Game {

    private OrthographicCamera camera;
    private World world;
    private Box2DDebugRenderer renderer;
    private Ball[] balls;
    private ShapeRenderer soundRenderer;

    public interface ForceListener {
        void forceImpact(float force);
    }

    public static class Ball implements ForceListener {
        private static final float MIN_FORCE = 200.0f;
        private static final float MAX_FORCE = 8000.0f;
        private Body body;
        private float impactForce = 0.0f;
        private float soundVolume = 0.0f;

        public Ball(World world, float x, float y) {
            CircleShape ballShape = new CircleShape();
            ballShape.setRadius(4.0f);

            FixtureDef ballFixtureDef = new FixtureDef();
            ballFixtureDef.shape = ballShape;
            ballFixtureDef.friction = 0.2f;
            ballFixtureDef.density = 1.0f;
            ballFixtureDef.restitution = 0.65f;

            BodyDef ballBodyDef = new BodyDef();
            ballBodyDef.type = BodyDef.BodyType.DynamicBody;

            body = world.createBody(ballBodyDef);
            Fixture ballFixture = body.createFixture(ballFixtureDef);
            ballFixture.setUserData(this);
            body.setTransform(x, y, 0);
        }

        public void update(float deltaTime) {
            soundVolume -= 8.0f * deltaTime;
            if (impactForce > MIN_FORCE) {
                float relativeForce = (Math.min(impactForce, MAX_FORCE) - MIN_FORCE) / (MAX_FORCE - MIN_FORCE);
                soundVolume = relativeForce * 4;
            }
            impactForce = 0.0f;
        }

        public void render(ShapeRenderer soundRenderer) {
            if (soundVolume > 0.0f) {
                soundRenderer.setColor(Color.RED);
                soundRenderer.circle(body.getPosition().x, body.getPosition().y, soundVolume, 16);
            }
        }

        @Override
        public void forceImpact(float force) {
            impactForce += force;
        }
    }

    private void buildGroundBlock(float x, float y) {
        PolygonShape groundShape = new PolygonShape();
        groundShape.setAsBox(5, 2);

        FixtureDef groundFixtureDef = new FixtureDef();
        groundFixtureDef.shape = groundShape;
        groundFixtureDef.friction = 0.2f;
        groundFixtureDef.density = 1.0f;
        groundFixtureDef.restitution = 0.2f;

        BodyDef groundBodyDef = new BodyDef();
        groundBodyDef.type = BodyDef.BodyType.StaticBody;

        Body ground = world.createBody(groundBodyDef);
        ground.createFixture(groundFixtureDef);
        ground.setTransform(x, y, 0);
    }

    @Override
    public void create() {
        float aspectRatio = (float) Gdx.graphics.getHeight() / (float) Gdx.graphics.getWidth();
        camera = new OrthographicCamera(100.0f, 100.0f * aspectRatio);
        camera.position.set(0.0f, 25.0f, 1.0f);
        camera.update();

        world = new World(new Vector2(0, -50), false);

        buildGroundBlock(-40.0f, 0.0f);
        buildGroundBlock(-25.0f, 0.0f);
        buildGroundBlock(25.0f, 0.0f);

        Ball ballA = new Ball(world, -32.5f, 50.0f);
        Ball ballB = new Ball(world, 25.0f, 75.0f);
        balls = new Ball[]{ballA, ballB};

        world.setContactListener(new ContactListener() {
            @Override
            public void beginContact(Contact contact) {
            }

            @Override
            public void endContact(Contact contact) {
            }

            @Override
            public void preSolve(Contact contact, Manifold oldManifold) {
            }

            @Override
            public void postSolve(Contact contact, ContactImpulse impulse) {
                Fixture fixtureA = contact.getFixtureA();
                Fixture fixtureB = contact.getFixtureB();

                Object userObjectA = fixtureA.getUserData();
                Object userObjectB = fixtureB.getUserData();

                int count = impulse.getCount();
                float totalForce = 0.0f;
                for (int i = 0; i < count; ++i) {
                    float fx = impulse.getNormalImpulses()[i * 2 + 0];
                    float fy = impulse.getNormalImpulses()[i * 2 + 1];
                    float force = (float) Math.sqrt(fx * fx + fy * fy);
                    totalForce += force;
                }

                if (userObjectA instanceof ForceListener)
                    ((ForceListener) userObjectA).forceImpact(totalForce);

                if (userObjectB instanceof ForceListener)
                    ((ForceListener) userObjectB).forceImpact(totalForce);
            }
        });

        soundRenderer = new ShapeRenderer();
        renderer = new Box2DDebugRenderer();
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(0, 0, 0, 0);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        world.step(Gdx.graphics.getDeltaTime(), 4, 6);

        for (Ball ball : balls)
            ball.update(Gdx.graphics.getDeltaTime());
        renderer.render(world, camera.combined);

        soundRenderer.setProjectionMatrix(camera.combined);
        soundRenderer.begin(ShapeRenderer.ShapeType.Filled);
        for (Ball ball : balls)
            ball.render(soundRenderer);

        soundRenderer.end();
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.