Matter.js soft bodies? Jelly

Does soft bodies work within Phaser 3? using Matter.js? I tried varying ways using the examples and such from matter.js however when i implement this into a simple .PNG cube character, the character appears to be in the environment because i can see it constantly translating in the console, however it does not appear in the spawning location so i cannot see it or verify if it works or not. Im just trying to implement a simple jelly effect to a .PNG cube.

Thanks
David

:wave:

Which example are you following? Show your code?

Thanks for the reply! :slight_smile: I’ll dig out the code shortly, though to quickly answer your question about the example; https://codepen.io/Shokeen/pen/EmOLJO

Are there code tags in this forum?

Thanks
David

class Player {
    constructor(scene, x, y) {
        this.scene = scene;

        const Matter = Phaser.Physics.Matter.Matter;
        const { Composites, Body, Bodies, Constraint } = Matter;

        const particleOptions = {
            friction: 0.05,
            frictionStatic: 0.1,
            render: { visible: false }
        };

        const constraintOptions = {
            stiffness: 0.06,  
            render: { visible: false }
        };

        this.softBody = Composites.softBody(x, y, 3, 3, 0, 0, true, 15, particleOptions, constraintOptions);
        scene.matter.world.add(this.softBody);

        this.square = scene.add.graphics();
        this.square.fillStyle(0x0000ff, 1);
        this.square.fillRect(-30, -30, 60, 60);  
        this.square.setPosition(x, y);
        console.log('Square created at:', { x, y });

        this.graphics = this.softBody.bodies.map(part => scene.add.circle(part.position.x, part.position.y, 3, 0x00ff00));

        // Create a 3x3 grid overlay on the mesh
        this.grid = [];
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                this.grid.push(scene.add.circle(x + (i - 1) * 10, y + (j - 1) * 10, 2, 0xff0000));
            }
        }

        this.canJump = true;
        this.isOnGround = false;

        this.lastKeyTime = {
            left: 0,
            right: 0
        };
        this.keyHeld = {
            left: false,
            right: false
        };
        this.isBoosting = false;
        this.doubleTapDelay = 250;

        scene.matter.world.on('collisionactive', this.handleCollision, this);
    }

    handleCollision(event) {
        event.pairs.forEach(pair => {
            const { bodyA, bodyB } = pair;
            if (this.softBody.bodies.includes(bodyA) || this.softBody.bodies.includes(bodyB)) {
                if (bodyA.isStatic || bodyB.isStatic) {
                    this.isOnGround = true;
                    console.log('Collision detected, player is on the ground.');
                }
            }
        });
    }

    checkDoubleTap(direction) {
        const currentTime = this.scene.time.now;
        if (currentTime - this.lastKeyTime[direction] < this.doubleTapDelay && !this.keyHeld[direction]) {
            this.isBoosting = true;
            this.lastKeyTime[direction] = 0;
            console.log('Double tap detected:', direction);
        } else {
            this.isBoosting = false;
        }
        this.lastKeyTime[direction] = currentTime;
    }

    update(cursors) {
        const { Body } = Phaser.Physics.Matter.Matter;

        const baseSpeed = 2.5;
        const speedBoost = 1.6;
        const baseJumpVelocity = -11;

        let playerSpeed = baseSpeed;
        let jumpVelocity = baseJumpVelocity;

        if (cursors.left.isDown) {
            if (!this.keyHeld.left) {
                this.checkDoubleTap('left');
            }
            this.keyHeld.left = true;
        } else {
            this.keyHeld.left = false;
        }

        if (cursors.right.isDown) {
            if (!this.keyHeld.right) {
                this.checkDoubleTap('right');
            }
            this.keyHeld.right = true;
        } else {
            this.keyHeld.right = false;
        }

        if (cursors.shift.isDown || this.isBoosting) {
            playerSpeed *= speedBoost;
            this.square.clear();  
            this.square.fillStyle(0xff0000, 1);  // Set fill color to red for speed boost
            this.square.fillRect(-30, -30, 60, 60);  
            console.log('Speed boost activated.');
        } else {
            this.square.clear();  
            this.square.fillStyle(0x0000ff, 1);  // Set fill color to blue
            this.square.fillRect(-30, -30, 60, 60);  
        }

        if (cursors.left.isDown) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: -playerSpeed, y: part.velocity.y }));
        } else if (cursors.right.isDown) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: playerSpeed, y: part.velocity.y }));
        } else {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: 0, y: part.velocity.y }));
        }

        if ((cursors.space.isDown || cursors.up.isDown) && this.isOnGround && this.canJump) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: part.velocity.x, y: jumpVelocity }));

            if (cursors.shift.isDown || this.isBoosting) {
                this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: part.velocity.x * speedBoost, y: part.velocity.y }));
            }

            this.canJump = false;
            this.isOnGround = false;
            console.log('Jump initiated.');
        }

        if (!cursors.space.isDown && !cursors.up.isDown) {
            this.canJump = true;
        }

        if (!cursors.left.isDown && !cursors.right.isDown) {
            this.isBoosting = false;
        }

        // Update the graphics positions
        this.softBody.bodies.forEach((part, index) => {
            this.graphics[index].setPosition(part.position.x, part.position.y);
        });

        // Calculate the rotation based on the soft body
        const [partA, partB, partC] = [this.softBody.bodies[0], this.softBody.bodies[1], this.softBody.bodies[2]];
        const angleAB = Math.atan2(partB.position.y - partA.position.y, partB.position.x - partA.position.x);
        const angleAC = Math.atan2(partC.position.y - partA.position.y, partC.position.x - partA.position.x);
        const angle = (angleAB + angleAC) / 2;

        // Update the square to follow the soft body and rotate accordingly
        const centerX = this.softBody.bodies.reduce((sum, part) => sum + part.position.x, 0) / this.softBody.bodies.length;
        const centerY = this.softBody.bodies.reduce((sum, part) => sum + part.position.y, 0) / this.softBody.bodies.length;
        this.square.setPosition(centerX, centerY);
        this.square.setRotation(angle);

        // Update the grid positions to match the mesh
        const gridSize = 30; 
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                const index = i * 3 + j;
                this.grid[index].setPosition(
                    centerX + (i - 1) * gridSize * Math.cos(angle) - (j - 1) * gridSize * Math.sin(angle),
                    centerY + (i - 1) * gridSize * Math.sin(angle) + (j - 1) * gridSize * Math.cos(angle)
                );
            }
        }

        // Debugging: Log the position and velocities of each body part
        this.softBody.bodies.forEach((part, index) => {
            console.log(`Soft body part ${index} - position:`, part.position, 'velocity:', part.velocity);
        });

        // Debugging: Log the center position of the player and the rotation
        console.log('Player center position:', { x: centerX, y: centerY });
        console.log('Player rotation:', angle);

        // Debugging: Log the vertices of the soft body
        this.softBody.bodies.forEach((part, index) => {
            console.log(`Soft body vertex ${index} - position:`, part.position);
        });
    }
}

Recording 2024-05-31 at 13.24.51

The green dots are the soft body deformation. The red dots are simply connected to the cube as a visual way of seeing if they deform.

Trying another solution:
rec2


class Player {
    constructor(scene, x, y) {
        this.scene = scene;

        const Matter = Phaser.Physics.Matter.Matter;
        const { Composites, Body, Bodies, Constraint } = Matter;

        const particleOptions = {
            friction: 0.05,
            frictionStatic: 0.1,
            render: { visible: false }
        };

        const constraintOptions = {
            render: { visible: false },
            stiffness: 0.06 
        };

        this.softBody = Composites.softBody(x, y, 3, 3, 0, 0, true, 15, particleOptions, constraintOptions);
        scene.matter.world.add(this.softBody);

        // Create a graphics object for the cube
        this.cube = scene.add.graphics();
        this.updateCubeGraphics();

        // Initialize graphics for debugging
        this.graphics = this.softBody.bodies.map(part => scene.add.circle(part.position.x, part.position.y, 3, 0x00ff00));

        this.canJump = true;
        this.isOnGround = false;

        this.lastKeyTime = {
            left: 0,
            right: 0
        };
        this.keyHeld = {
            left: false,
            right: false
        };
        this.isBoosting = false;
        this.doubleTapDelay = 250;

        scene.matter.world.on('collisionactive', this.handleCollision, this);
    }

    handleCollision(event) {
        event.pairs.forEach(pair => {
            const { bodyA, bodyB } = pair;
            if (this.softBody.bodies.includes(bodyA) || this.softBody.bodies.includes(bodyB)) {
                if (bodyA.isStatic || bodyB.isStatic) {
                    this.isOnGround = true;
                    console.log('Collision detected, player is on the ground.');
                }
            }
        });
    }

    checkDoubleTap(direction) {
        const currentTime = this.scene.time.now;
        if (currentTime - this.lastKeyTime[direction] < this.doubleTapDelay && !this.keyHeld[direction]) {
            this.isBoosting = true;
            this.lastKeyTime[direction] = 0;
            console.log('Double tap detected:', direction);
        } else {
            this.isBoosting = false;
        }
        this.lastKeyTime[direction] = currentTime;
    }

    update(cursors) {
        const { Body } = Phaser.Physics.Matter.Matter;

        const baseSpeed = 2.5;
        const speedBoost = 1.6;
        const baseJumpVelocity = -11;

        let playerSpeed = baseSpeed;
        let jumpVelocity = baseJumpVelocity;

        if (cursors.left.isDown) {
            if (!this.keyHeld.left) {
                this.checkDoubleTap('left');
            }
            this.keyHeld.left = true;
        } else {
            this.keyHeld.left = false;
        }

        if (cursors.right.isDown) {
            if (!this.keyHeld.right) {
                this.checkDoubleTap('right');
            }
            this.keyHeld.right = true;
        } else {
            this.keyHeld.right = false;
        }

        if (cursors.shift.isDown || this.isBoosting) {
            playerSpeed *= speedBoost;
            this.cube.fillStyle(0xff0000); // Set fill color to red for speed boost
            console.log('Speed boost activated.');
        } else {
            this.cube.fillStyle(0x0000ff); // Set fill color to blue when not boosting
        }

        if (cursors.left.isDown) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: -playerSpeed, y: part.velocity.y }));
        } else if (cursors.right.isDown) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: playerSpeed, y: part.velocity.y }));
        } else {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: 0, y: part.velocity.y }));
        }

        if ((cursors.space.isDown || cursors.up.isDown) && this.isOnGround && this.canJump) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: part.velocity.x, y: jumpVelocity }));

            if (cursors.shift.isDown || this.isBoosting) {
                this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: part.velocity.x * speedBoost, y: part.velocity.y }));
            }

            this.canJump = false;
            this.isOnGround = false;
            console.log('Jump initiated.');
        }

        if (!cursors.space.isDown && !cursors.up.isDown) {
            this.canJump = true;
        }

        if (!cursors.left.isDown && !cursors.right.isDown) {
            this.isBoosting = false;
        }

        // Update the graphics positions
        this.softBody.bodies.forEach((part, index) => {
            this.graphics[index].setPosition(part.position.x, part.position.y);
        });

        // Update the cube graphics
        this.updateCubeGraphics();

        // Debugging: Log the position and velocities of each body part
        this.softBody.bodies.forEach((part, index) => {
            console.log(`Soft body part ${index} - position:`, part.position, 'velocity:', part.velocity);
        });

        // Debugging: Log the center position of the player and the rotation
        const centerX = this.softBody.bodies.reduce((sum, part) => sum + part.position.x, 0) / this.softBody.bodies.length;
        const centerY = this.softBody.bodies.reduce((sum, part) => sum + part.position.y, 0) / this.softBody.bodies.length;
        console.log('Player center position:', { x: centerX, y: centerY });

        // Debugging: Log the vertices of the soft body
        this.softBody.bodies.forEach((part, index) => {
            console.log(`Soft body vertex ${index} - position:`, part.position);
        });
    }

    updateCubeGraphics() {
        this.cube.clear();
        this.cube.fillStyle(0x0000ff, 1.0);

        // Draw the cube based on the positions of the soft body vertices
        this.cube.beginPath();
        this.cube.moveTo(this.softBody.bodies[0].position.x, this.softBody.bodies[0].position.y);
        this.cube.lineTo(this.softBody.bodies[2].position.x, this.softBody.bodies[2].position.y);
        this.cube.lineTo(this.softBody.bodies[8].position.x, this.softBody.bodies[8].position.y);
        this.cube.lineTo(this.softBody.bodies[6].position.x, this.softBody.bodies[6].position.y);
        this.cube.closePath();
        this.cube.fillPath();
    }
}

rec3

I cant seem to deform the middle points. Also the particle sizes for the soft bodies create this gap between the cube and the contact surface, i try to reduce the size and the cube becomes very unstable :frowning:

Matter.js is a rigid body physics simulation.
The soft body object is a “hack” that follows this example.
If you want to reproduce a true soft body behavior, you have to implement it yourself using multiple tiny bodies and joints that would similar to this.
image

Otherwise, try to find another engine that supprorts soft bodies.

Yup, like a particle system? That’s what i currently have. Its mostly working as expected. Basically im trying to create a pillow that has deformation around surface contact edges. However i get a lot of particle penetration occurring which doesn’t restore to the original shape. I’m currently trying to find a way to ensure the shape returns to its original shape, currently without success. … Sub poly tessellation maybe …

Recording 2024-06-01 at 11.56.07

class Player {
    constructor(scene, x, y) {
        this.scene = scene;

        const Matter = Phaser.Physics.Matter.Matter;
        const { Composites, Composite, Body, Bodies, Constraint } = Matter;

        const particleOptions = {
            friction: 0.05,
            frictionStatic: 0.5,
            render: { visible: false },
            restitution: 1.0 
        };

        const constraintOptions = {
            render: { visible: false },
            stiffness: 0.2, 
            damping: 0.06, 
            angularStiffness: 0.1 
        };

        // Create the soft body
        this.softBody = Composites.softBody(x, y, 4, 4, 10, 10, true, 9, particleOptions, constraintOptions);
        scene.matter.world.add(this.softBody);

        // Adjust the stiffness for the outer particles
        Composite.allConstraints(this.softBody).forEach(constraint => {
            const { bodyA, bodyB } = constraint;
            const isOuterA = this.isOuterParticle(bodyA.position, x, y, 4, 4);
            const isOuterB = this.isOuterParticle(bodyB.position, x, y, 4, 4);

            if (isOuterA || isOuterB) {
                constraint.stiffness = 0.3;
            }
        });

        // Create a graphics object for the character mesh.
        this.cube = scene.add.graphics({ fillStyle: { color: 0x0000ff } });
        this.updateCubeGraphics(); // Initial call to set graphics

        // Initialize graphics for debugging and make circles visible
        this.graphics = this.softBody.bodies.map(part => {
            const circle = scene.add.circle(part.position.x, part.position.y, 3, 0x00ff00);
            circle.setAlpha(1); 
            return circle;
        });

        // Create a target cube for the camera to follow
        this.targetCube = scene.add.rectangle(x, y, 10, 10, 0xff0000);
        this.targetCube.setAlpha(0); 

        this.canJump = true;
        this.isOnGround = false;

        this.lastKeyTime = {
            left: 0,
            right: 0
        };
        this.keyHeld = {
            left: false,
            right: false
        };
        this.isBoosting = false;
        this.doubleTapDelay = 250;

        scene.matter.world.setGravity(0, 1.1);

        scene.matter.world.on('collisionactive', this.handleCollision, this);

        // Debugging information
        console.log('Player created at:', x, y);
    }

    isOuterParticle(position, x, y, rows, cols) {
        const margin = 15; 
        const left = x - margin;
        const right = x + (cols - 1) * 10 + margin;
        const top = y - margin;
        const bottom = y + (rows - 1) * 10 + margin;

        return (
            position.x <= left || position.x >= right || position.y <= top || position.y >= bottom
        );
    }

    handleCollision(event) {
        event.pairs.forEach(pair => {
            const { bodyA, bodyB } = pair;
            if (this.softBody.bodies.includes(bodyA) || this.softBody.bodies.includes(bodyB)) {
                if (bodyA.isStatic || bodyB.isStatic) {
                    this.isOnGround = true;
                }
            }
        });
    }

    checkDoubleTap(direction) {
        const currentTime = this.scene.time.now;
        if (currentTime - this.lastKeyTime[direction] < this.doubleTapDelay && !this.keyHeld[direction]) {
            this.isBoosting = true;
            this.lastKeyTime[direction] = 0;
        } else {
            this.isBoosting = false;
        }
        this.lastKeyTime[direction] = currentTime;
    }

    update(cursors) {
        const { Body } = Phaser.Physics.Matter.Matter;

        const baseSpeed = 2.5;
        const speedBoost = 1.6;
        const baseJumpVelocity = -11;

        let playerSpeed = baseSpeed;
        let jumpVelocity = baseJumpVelocity;

        if (cursors.left.isDown) {
            if (!this.keyHeld.left) {
                this.checkDoubleTap('left');
            }
            this.keyHeld.left = true;
        } else {
            this.keyHeld.left = false;
        }

        if (cursors.right.isDown) {
            if (!this.keyHeld.right) {
                this.checkDoubleTap('right');
            }
            this.keyHeld.right = true;
        } else {
            this.keyHeld.right = false;
        }

        if (cursors.shift.isDown || this.isBoosting) {
            playerSpeed *= speedBoost;
            this.cube.fillStyle(0xff0000); // Set fill color to red for speed boost
        } else {
            this.cube.fillStyle(0x0000ff); // Set fill color to blue when not boosting
        }

        if (cursors.left.isDown) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: -playerSpeed, y: part.velocity.y }));
        } else if (cursors.right.isDown) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: playerSpeed, y: part.velocity.y }));
        } else {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: 0, y: part.velocity.y }));
        }

        if ((cursors.space.isDown || cursors.up.isDown) && this.isOnGround && this.canJump) {
            this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: part.velocity.x, y: jumpVelocity }));

            if (cursors.shift.isDown || this.isBoosting) {
                this.softBody.bodies.forEach(part => Body.setVelocity(part, { x: part.velocity.x * speedBoost, y: part.velocity.y }));
            }

            this.canJump = false;
            this.isOnGround = false;
        }

        if (!cursors.space.isDown && !cursors.up.isDown) {
            this.canJump = true;
        }

        if (!cursors.left.isDown && !cursors.right.isDown) {
            this.isBoosting = false;
        }

        // Update the graphics positions
        this.softBody.bodies.forEach((part, index) => {
            this.graphics[index].setPosition(part.position.x, part.position.y);
        });

        // Update the target cube position
        const targetPosition = this.softBody.bodies[4].position;
        this.targetCube.setPosition(targetPosition.x, targetPosition.y);

        // Update the cube graphics without distortion
        this.updateCubeGraphics();
    }

    updateCubeGraphics() {
        this.cube.clear();
        this.cube.fillStyle(0x0000ff, 1.0);

        // Get positions of the soft body vertices
        const positions = this.softBody.bodies.map(body => ({ x: body.position.x, y: body.position.y }));

        // Draw individual small cubes for the polygon mesh structure.
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                const topLeft = positions[i * 4 + j];
                const topRight = positions[i * 4 + j + 1];
                const bottomRight = positions[(i + 1) * 4 + j + 1];
                const bottomLeft = positions[(i + 1) * 4 + j];

                this.cube.beginPath();
                this.cube.moveTo(topLeft.x, topLeft.y);
                this.cube.lineTo(topRight.x, topRight.y);
                this.cube.lineTo(bottomRight.x, bottomRight.y);
                this.cube.lineTo(bottomLeft.x, bottomLeft.y);
                this.cube.closePath();
                this.cube.fillPath();
            }
        }

        // Log position for debugging
        const centerX = positions.reduce((sum, pos) => sum + pos.x, 0) / positions.length;
        const centerY = positions.reduce((sum, pos) => sum + pos.y, 0) / positions.length;
        console.log('Cube position:', centerX, centerY);
    }
}

window.Player = Player;

I will point you to this video which is a great overview on how to implement soft bodies
Physics of JellyCar: Soft Body Physics Explained