Implementing a shatter destruction effect using Box2D and XNA

I’ve had a couple requests to share how I implemented the shattering effect in these two videos:

It’s pretty straightforward to accomplish, and can easily be tweaked to accommodate your aesthetic and performance needs. I’m assuming a basic familiarity with Farseer (the XNA port of Box2D) and the XNA graphics libraries.

To begin, dispose of whatever Body you were using to track the unshattered object, if any. We’re going to replace it with a bunch of smaller bodies that comprise the shards of the original object. Each of those shards will be encapsulated with a simple container class that tracks its Body and its coordinates on the source image.

private class Piece {
    public Piece(Body body, int x, int y) {
        Body = body;
        X = x;
        Y = y;
    }

    public Body Body { get; private set; }
    public int X { get; private set; }
    public int Y { get; private set; }
}

We do a bit of math to determine where each of these pieces should be centered, according to the simulation position of the original image’s center. ConvertUnits is a Box2D design pattern that knows how to convert a measurement from screen space to simulation space and back. There’s a complete implementation you can use in the Farseer sample code. This implementation uses the same number of cuts in the vertical and horizontal directions, which will result in non-square shards if you feed it a non-square image.

/// 
/// Begins a new destruction animation for the image given.
/// 
/// The simulation world
/// The image to destroy
/// The rectangle on the texture to split up and draw
/// The center of the original image
/// The number of horizontal and vertical pieces to split the image into
/// The maximum velocity of any individual piece
public ShatterAnimation(World world, Texture2D image, Rectangle? originRectangle, Vector2 position, int numPieces, float maxVelocity) {
    _image = image;
    _originRectangle = originRectangle ?? new Rectangle(0, 0, 0, 0);
    _pieces = new Piece[numPieces * numPieces];
    _maxVelocity = maxVelocity;

    int width = originRectangle == null ? _image.Width : originRectangle.Value.Width;
    int height = originRectangle == null ? _image.Height : originRectangle.Value.Height;

    _displayPieceWidth = width / numPieces;
    float simPieceWidth = ConvertUnits.ToSimUnits(_displayPieceWidth);
    _displayPieceHeight = height / numPieces;
    float simPieceHeight = ConvertUnits.ToSimUnits(_displayPieceHeight);

    int displayWidthOffset = width / 2;
    float simWidthOffset = ConvertUnits.ToSimUnits(displayWidthOffset);
    int displayHeightOffset = height / 2;
    float simHeightOffset = ConvertUnits.ToSimUnits(displayHeightOffset);

    int i = 0;
    for ( int x = 0; x < numPieces; x++ ) {
        for ( int y = 0; y < numPieces; y++ ) {
            float posx = position.X - simWidthOffset + (x * simPieceWidth + simPieceWidth / 2);
            float posy = position.Y - simHeightOffset + (y * simPieceHeight + simPieceHeight / 2);
            Body body = BodyFactory.CreateRectangle(world, simPieceWidth * 3f / 4f, simPieceHeight * 3f / 4f, 1);
            body.CollidesWith = Arena.TerrainCategory;
            body.CollisionCategories = Arena.TerrainCategory;
            body.Position = new Vector2(posx, posy);
            body.IsStatic = false;
            body.FixedRotation = false;
            body.Restitution = .6f;
            AssignRandomDirection(body);
            _pieces[i++] = new Piece(body, x, y);
        }
    }
}

private void AssignRandomDirection(Body body) {
    double linearVelocity = random.NextDouble() * _maxVelocity;
    double direction = random.NextDouble() * Math.PI * 2;
    Vector2 velocity = new Vector2((float) (Math.Cos(direction) * linearVelocity),
        (float) (Math.Sin(direction) * linearVelocity));
    body.LinearVelocity = velocity;
}

To draw the effect, we take the position and rotation of the simulated Body for each shard, translate it to screen space, and tell XNA which slice of the source image to draw on top.

public void Draw(SpriteBatch spriteBatch) {
    float alpha = 1f - (float) _timeAlive / (float) TimeToLiveMs;
    int xOffset = _originRectangle.X;
    int yOffset = _originRectangle.Y;
    foreach ( Piece piece in _pieces ) {
        Vector2 displayPosition = ConvertUnits.ToDisplayUnits(piece.Body.Position);
        Vector2 origin = new Vector2(_displayPieceWidth / 2, _displayPieceHeight / 2);
        float rotation = piece.Body.Rotation;
        spriteBatch.Draw(_image,
                            new Rectangle((int) displayPosition.X, (int) displayPosition.Y, _displayPieceWidth,
                                        _displayPieceHeight),
                            new Rectangle(xOffset + _displayPieceWidth * piece.X, yOffset + _displayPieceHeight * piece.Y, _displayPieceWidth,
                                        _displayPieceHeight),
                            Color.White * alpha, rotation, origin,
                            SpriteEffects.None, 0);
    }
}

My implementation uses a fade-away effect. To get this to work, you need to track how long the animation has been running so you can calculate the alpha value to draw.

public void Update(GameTime gameTime) {
    _timeAlive += gameTime.ElapsedGameTime.Milliseconds;
}

Finally, don't forget to Dispose of the Body's once the effect is over!

There are lots of ways you can tweak the effect to make it more suitable for whatever you're doing with it. The most important parameters are the number of pieces, what they collide with, the restitution (bounciness) of the pieces, their speed, and their margin. The code that controls these factors is reproduced below.

Body body = BodyFactory.CreateRectangle(world, simPieceWidth * 3f / 4f, simPieceHeight * 3f / 4f, 1);
body.CollidesWith = Arena.TerrainCategory;
body.CollisionCategories = Arena.TerrainCategory;
body.Restitution = .6f;
AssignRandomDirection(body);

In my implementation, I chose relatively bouncy pieces with a reasonable amount of padding around them (1/4 the piece size). Less bouncy pieces with less padding (and with a lower velocity) tend to clump together and fall over, which looks kind of neat on free-roaming enemies but strange when applied to blocks. It's also possible to make the shards collide with whatever you want in your game world, or with nothing at all. My current implementation has them collide with each other and the terrain, but nothing else. Because Farseer performance depends heavily on the number of simultaneous contacts, making them not collide with one another is an easy way to boost performance, along with decreasing the number of shards.

Let me know in the comments if you have any questions, or show off how you're using this in your own games!

Leave a Reply