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
- 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.
- In the
userObject
, store the information of all such notifications.
- In an
update
method that runs once per game-loop tick figure out if a sounds needs to be played and play it.
- 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:
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();
}
}