I'm trying to create a ball in matter-js that will bounce off rectangles and jump to the next rectangle. All I can know is ball's position and next rectangle's position. I found a similar question at unity forum. I tried to implement code from Iron-Warrior's solution in my project, but I have problem that my ball constantly jumps strongly over the next rectangle.
Here is the code that i got:
const ball = Bodies.circle(0, 0, 10);
const rectangles = Array(20)
.fill(0)
.map((_, i) => Bodies.rectangle(i * 140, 200, 20, 20, { isStatic: true }));
Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((pair) => {
if (pair.bodyA === ball) {
const target = rectangles[rectangles.indexOf(pair.bodyB) + 1];
const gravity = engine.gravity.y;
const initialAngle = 60;
const yOffset = ball.position.y - target.position.y;
const angle = initialAngle * (Math.PI / 180);
const distance = Vector.magnitude(Vector.sub(ball.position, target.position));
const initialVelocity =
(1 / Math.cos(angle)) *
Math.sqrt(
(0.5 * gravity * Math.pow(distance, 2)) / (distance * Math.tan(angle) + yOffset),
);
const velocity = {
x: initialVelocity * Math.cos(angle),
y: -initialVelocity * Math.sin(angle),
};
const angleBetweenObjects = Vector.angle(ball.position, target.position);
const finalVelocity = Vector.rotate(velocity, angleBetweenObjects);
Body.setVelocity(ball, finalVelocity);
}
});
});
Composite.add(engine.world, [ball, ...rectangles]);
Tried to increase engine.velocityIterations
Here is codesandbox example: https://codesandbox.io/p/sandbox/relaxed-shape-9wy3kt
I believe this is mostly matter-js problem. I tried to change formula, change ball options, rectangle options, nothing helps. Maybe there is some alternative engine to matterjs?
Finally cracked it! The main difficulty was to see the fact that
if you setVelocity
in the collisionStart
, the effect of
the collision is still applied to the ball, so its set velocity
was altered, hence the unpredictability of its trajectory.
Thus, the main change is to move the action to collisionActive
handler.
There are also some minor additions and corrections to be made to your code:
target.bounds.min.y
in this reference system with y increasing downwards) and the center of the ball
should be +ball.circleRadius
above that.distance
by its x component. I used variables dx
and dy
(instead of yOffset
) for the two components of the distance, with which the formula of the initial velocity needed to throw the ball
to the target
(see for instance projectile motion) might be written as:
dx = target.position.x - ball.position.x
dy = target.bounds.min.y - ball.position.y - ball.circleRadius
initialVelocity = dx / Math.cos(angle) *
Math.sqrt((0.5 * gravity) / (dy + dx * Math.tan(angle)));
velocity = Vector.create(
initialVelocity * Math.cos(angle), // vx
-initialVelocity * Math.sin(angle) // vy
);
matter.js
is
actually engine.gravity.y * engine.gravity.scale
; that needs to be multiplied by the factor Body._baseDelta ** 2
where Body._baseDelta
is the basic time unit of the engine. I derived this correction factor by looking at the source code of matter.js
and it's a peculiarity (not to say a bug) of this library.ball.airFriction = 0
;
while taking into consideration the air friction is theoretically
feasible, that is a much more difficult proposition.Body.setAngularVelocity(ball, 0)
-- although the spin
of the ball does not affect its energy, there is a funny
effect of the rotation, that the ball although touches the
rectangle in the right position there is no collision and the
ball somehow slides off the surface.Here it goes in a code snippet, with some random positioning of the rectangles and a simple reverse of the direction when the ball reaches the last/first rectangle:
const Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
Bodies = Matter.Bodies,
Composite = Matter.Composite,
Vector = Matter.Vector,
Body = Matter.Body,
World = Matter.World,
Events = Matter.Events;
// create an engine
const engine = Engine.create();
//engine.positionIterations = 200;
//engine.velocityIterations = 200;
engine.enableSleeping = true;
const width = 800, height = 300;
// create a renderer
const render = Render.create({
element: document.body,
engine: engine,
options: {
width,
height,
showAngleIndicator: true,
showVelocity: true,
},
});
const ball = Bodies.circle(0, 0, 10);
ball.frictionAir = 0;
let reverse = false;
const intRnd = (min, max) => Math.floor(min + Math.random() * (max - min));
const rectangles = Array(60)
.fill(0)
.map((_, i) =>
Bodies.rectangle(i * 150 + (i === 0 ? 0 : intRnd(-30, 30)), 150 + intRnd(-50, 50), 30, 30, {isStatic: true}),
);
Events.on(engine, "collisionActive", (event) => {
event.pairs.forEach((pair) => {
if (pair.bodyA === ball) {
let target = rectangles[rectangles.indexOf(pair.bodyB) + (reverse ? -1 : 1)];
if(!target){
reverse = !reverse;
target = rectangles[rectangles.indexOf(pair.bodyB) + (reverse ? -1 : 1)];
}
const gravity = engine.gravity.y * engine.gravity.scale * Body._baseDelta * Body._baseDelta;
const initialAngle = reverse ? -60 : 60;
const angle = deg2rad(initialAngle);
const dx = target.position.x - ball.position.x,
dy = target.bounds.min.y - ball.position.y - ball.circleRadius;
const initialVelocity = dx / Math.cos(angle) * Math.sqrt((0.5 * gravity) / (dy + dx * Math.tan(angle)));
if (initialVelocity) {
const velocity = Vector.create(
initialVelocity * Math.cos(angle),
-initialVelocity * Math.sin(angle),
);
Body.setVelocity(ball, velocity);
}
else{
Runner.stop(runner)
}
Body.setAngularVelocity(ball, 0);
}
});
});
Composite.add(engine.world, [ball, ...rectangles]);
function deg2rad(degrees) {
return degrees * (Math.PI / 180);
}
// create runner
const runner = Runner.create();
const updateCameraPosition = () => {
// Calculate camera position based on ball's position - just x
if (ball.position.y > height * 0.9) {
stop();
}
const xOffset = ball.position.x - width / 2;
Render.lookAt(render, {
min: {x: xOffset, y: 0},
max: {
x: xOffset + width,
y: height,
},
});
};
Events.on(engine, "afterUpdate", () => {
updateCameraPosition();
});
const stop = function(){
Render.stop(render);
World.clear(engine.world, false);
Engine.clear(engine);
document.querySelector('#stop').style.visibility = 'hidden';
}
// run the renderer
Render.run(render);
// run the engine
Runner.run(runner, engine);
// setTimeout(() => {
// stop()
// }, 300000) // stop after 5min
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.js" integrity="sha512-rsntMCBgWYEWKl4HtqWmQ3vdHgvSq8CTtJd19YL7lCtKokLPWt7UEoHVabK1uiNfUdaLit8O090no5BZjcz+bw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<button id='stop' onclick="stop()">Stop</button>
or a jsFiddle.