Evite el arrastre forzado de cuerpos a través de otros cuerpos con MatterJS

14

Estoy usando MatterJs para un juego basado en la física y no he encontrado una solución para el problema de evitar que los cuerpos sean arrastrados por el mouse a otros cuerpos. Si arrastra un cuerpo hacia otro cuerpo, el cuerpo que está siendo arrastrado puede forzarse dentro del otro cuerpo y atravesarlo. Estoy buscando una forma confiable de evitar que se crucen. Puede observar este efecto en cualquier demostración de MatterJS seleccionando un cuerpo con el mouse e intentando forzarlo a través de otro cuerpo. Aquí está un ejemplo típico:

ingrese la descripción de la imagen aquí

https://brm.io/matter-js/demo/#staticFriction

Desafortunadamente, esto rompe cualquier juego o simulación dependiendo de arrastrar y soltar. He intentado numerosas soluciones, como romper la restricción del mouse cuando se produce una colisión o reducir la rigidez de la restricción, pero nada que funcione de manera confiable.

Cualquier sugerencia bienvenida!

d13
fuente
No entiendo la redacción arrastrada por la fuerza. ¿Quieres decir que tu cuerpo arrastrado debería pasar por otros cuerpos?
grodzi
No, significa que se debe evitar que el cuerpo arrastrado atraviese otros cuerpos.
d13
1
@ d13 ¿Podría agregar una animación que muestre el problema? Dado que parece haber cierta confusión basada en la redacción ...
Fantasma
2
@Ghost agregado ...
d13
@ d13 Eso aclara las cosas ..... esto es complicado
Ghost

Respuestas:

6

Creo que la mejor respuesta aquí sería una revisión importante del Matter.Resolvermódulo para implementar la prevención predictiva de conflictos físicos entre cualquier cuerpo. Cualquier cosa por debajo de eso está garantizada para fallar bajo ciertas circunstancias. Dicho esto aquí hay dos "soluciones" que, en realidad, son solo soluciones parciales. Se detallan a continuación.


Solución 1 (Actualización)

Esta solución tiene varias ventajas:

  • Es más conciso que la Solución 2
  • Crea una huella computacional más pequeña que Solución 2
  • El comportamiento de arrastre no se interrumpe como está en Solución 2
  • Se puede combinar de forma no destructiva con Solución 2

La idea detrás de este enfoque es resolver la paradoja de lo que sucede " cuando una fuerza imparable se encuentra con un objeto inamovible " haciendo que la fuerza se pueda detener. Esto está habilitado por Matter.Event beforeUpdate, que permite que la velocidad absoluta y el impulso (o más bien positionImpulse, que no es realmente un impulso físico) en cada dirección se vean restringidos dentro de los límites definidos por el usuario.

window.addEventListener('load', function() {
    var canvas = document.getElementById('world')
    var mouseNull = document.getElementById('mouseNull')
    var engine = Matter.Engine.create();
    var world = engine.world;
    var render = Matter.Render.create({    element: document.body, canvas: canvas,
                 engine: engine, options: { width: 800, height: 800,
                     background: 'transparent',showVelocity: true }});
    var body = Matter.Bodies.rectangle(400, 500, 200, 60, { isStatic: true}), 
        size = 50, counter = -1;
     
    var stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6, 
                                        0, 0, function(x, y) {
     return Matter.Bodies.rectangle(x, y, size * 2, size, {
         slop: 0, friction: 1,    frictionStatic: Infinity });
    });
    Matter.World.add(world, [ body, stack,
     Matter.Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
     Matter.Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),
     Matter.Bodies.rectangle(800, 300, 50, 600, { isStatic: true }),
     Matter.Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
    ]);

    Matter.Events.on(engine, 'beforeUpdate', function(event) {
     counter += 0.014;
     if (counter < 0) { return; }
     var px = 400 + 100 * Math.sin(counter);
     Matter.Body.setVelocity(body, { x: px - body.position.x, y: 0 });
     Matter.Body.setPosition(body, { x: px, y: body.position.y });
     if (dragBody != null) {
        if (dragBody.velocity.x > 25.0) {
            Matter.Body.setVelocity(dragBody, {x: 25, y: dragBody.velocity.y });
        }
        if (dragBody.velocity.y > 25.0) {
            Matter.Body.setVelocity(dragBody, {x: dragBody.velocity.x, y: 25 });
        }
        if (dragBody.positionImpulse.x > 25.0) {
            dragBody.positionImpulse.x = 25.0;
        }
        if (dragBody.positionImpulse.y > 25.0) {
            dragBody.positionImpulse.y = 25.0;
        }
    }
    });

    var mouse = Matter.Mouse.create(render.canvas),
     mouseConstraint = Matter.MouseConstraint.create(engine, { mouse: mouse,
         constraint: { stiffness: 0.1, render: { visible: false }}});
     
    var dragBody = null


    Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
     dragBody = event.body;
    });
    
    Matter.World.add(world, mouseConstraint);
    render.mouse = mouse;
    Matter.Engine.run(engine);
    Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.js"></script>

En el ejemplo, estoy restringiendo velocityy positionImpulseen xy ya una magnitud máxima de 25.0. El resultado se muestra a continuación.

ingrese la descripción de la imagen aquí

Como puede ver, es posible ser bastante violento al arrastrar los cuerpos y no se atravesarán entre sí. Esto es lo que diferencia a este enfoque de los demás: la mayoría de las otras soluciones potenciales fallan cuando el usuario es lo suficientemente violento con su arrastre.

El único inconveniente que he encontrado con este método es que es posible usar un cuerpo no estático para golpear a otro cuerpo no estático lo suficientemente fuerte como para darle suficiente velocidad al punto donde el Resolvermódulo no detectará la colisión y permitirá segundo cuerpo para pasar a través de otros cuerpos. (En el ejemplo de fricción estática, la velocidad requerida es de alrededor 50.0, solo he logrado hacer esto con éxito una vez y, en consecuencia, no tengo una animación que lo represente).


Solución 2

Sin embargo, esta es una solución adicional, una advertencia justa: no es sencilla.

En términos generales, la forma en que esto funciona es verificar si el cuerpo que se está arrastrando dragBody, ha chocado con un cuerpo estático y si el mouse se ha movido demasiado lejos sin dragBodyseguirlo. Si detecta que la separación entre el ratón y dragBodyse ha convertido en demasiado grande que elimina el detector de eventos desde y lo reemplaza con una función diferente mousemove, . Esta función verifica si el mouse ha regresado a una proximidad determinada del centro del cuerpo. Desafortunadamente, no pude lograr que el método incorporado funcionara correctamente, así que tuve que incluirlo directamente (alguien con más conocimientos que yo en Javascript tendrá que resolverlo). Finalmente, si se detecta un evento, vuelve al oyente normal .Matter.js mouse.mousemovemouse.elementmousemove()Matter.Mouse._getRelativeMousePosition()mouseupmousemove

window.addEventListener('load', function() {
    var canvas = document.getElementById('world')
    var mouseNull = document.getElementById('mouseNull')
    var engine = Matter.Engine.create();
    var world = engine.world;
    var render = Matter.Render.create({ element: document.body, canvas: canvas,
                 engine: engine, options: { width: 800, height: 800,
                     background: 'transparent',showVelocity: true }});
    var body = Matter.Bodies.rectangle(400, 500, 200, 60, { isStatic: true}), 
        size = 50, counter = -1;
     
    var stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6, 
                                        0, 0, function(x, y) {
     return Matter.Bodies.rectangle(x, y, size * 2, size, {
         slop: 0.5, friction: 1,    frictionStatic: Infinity });
    });
    Matter.World.add(world, [ body, stack,
     Matter.Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
     Matter.Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),
     Matter.Bodies.rectangle(800, 300, 50, 600, { isStatic: true }),
     Matter.Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
    ]);

    Matter.Events.on(engine, 'beforeUpdate', function(event) {
     counter += 0.014;
     if (counter < 0) { return; }
     var px = 400 + 100 * Math.sin(counter);
     Matter.Body.setVelocity(body, { x: px - body.position.x, y: 0 });
     Matter.Body.setPosition(body, { x: px, y: body.position.y });
    });

    var mouse = Matter.Mouse.create(render.canvas),
     mouseConstraint = Matter.MouseConstraint.create(engine, { mouse: mouse,
         constraint: { stiffness: 0.2, render: { visible: false }}});
     
    var dragBody, overshoot = 0.0, threshold = 50.0, loc, dloc, offset, 
    bodies = Matter.Composite.allBodies(world), moveOn = true;
    getMousePosition = function(event) {
     var element = mouse.element, pixelRatio = mouse.pixelRatio, 
        elementBounds = element.getBoundingClientRect(),
        rootNode = (document.documentElement || document.body.parentNode || 
                    document.body),
        scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : 
                   rootNode.scrollLeft,
        scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : 
                   rootNode.scrollTop,
        touches = event.changedTouches, x, y;
     if (touches) {
         x = touches[0].pageX - elementBounds.left - scrollX;
         y = touches[0].pageY - elementBounds.top - scrollY;
     } else {
         x = event.pageX - elementBounds.left - scrollX;
         y = event.pageY - elementBounds.top - scrollY;
     }
     return { 
         x: x / (element.clientWidth / (element.width || element.clientWidth) *
            pixelRatio) * mouse.scale.x + mouse.offset.x,
         y: y / (element.clientHeight / (element.height || element.clientHeight) *
            pixelRatio) * mouse.scale.y + mouse.offset.y
     };
    };     
    mousemove = function() {
     loc = getMousePosition(event);
     dloc = dragBody.position;
     overshoot = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5 - offset;
     if (overshoot < threshold) {
         mouse.element.removeEventListener("mousemove", mousemove);
         mouse.element.addEventListener("mousemove", mouse.mousemove);
         moveOn = true;
     }
    }
    Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
     dragBody = event.body;
     loc = mouse.position;
     dloc = dragBody.position;
     offset = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5;
     Matter.Events.on(mouseConstraint, 'mousemove', function(event) {
         loc = mouse.position;
         dloc = dragBody.position;
         for (var i = 0; i < bodies.length; i++) {                      
             overshoot = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5 - offset;
             if (bodies[i] != dragBody && 
                 Matter.SAT.collides(bodies[i], dragBody).collided == true) {
                 if (overshoot > threshold) {
                     if (moveOn == true) {
                         mouse.element.removeEventListener("mousemove", mouse.mousemove);
                         mouse.element.addEventListener("mousemove", mousemove);
                         moveOn = false;
                     }
                 }
             }
         }
     });
    });

    Matter.Events.on(mouseConstraint, 'mouseup', function(event) {
     if (moveOn == false){
         mouse.element.removeEventListener("mousemove", mousemove);
         mouse.element.addEventListener("mousemove", mouse.mousemove);
         moveOn = true;
     }
    });
    Matter.Events.on(mouseConstraint, 'enddrag', function(event) {
     overshoot = 0.0;
     Matter.Events.off(mouseConstraint, 'mousemove');
    });

    Matter.World.add(world, mouseConstraint);
    render.mouse = mouse;
    Matter.Engine.run(engine);
    Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.js"></script>

Después de aplicar el esquema de cambio de escucha de eventos, los cuerpos ahora se comportan más así

ingrese la descripción de la imagen aquí

He probado esto bastante a fondo, pero no puedo garantizar que funcione en todos los casos. También mouseupvale la pena señalar que el evento no se detecta a menos que el mouse esté dentro del lienzo cuando ocurre, pero esto es cierto para cualquier mouseupdetección de Matter.js , por lo que no intenté solucionarlo.

Si la velocidad es lo suficientemente grande, Resolverno detectará ninguna colisión y, dado que carece de prevención predictiva de este sabor de conflicto físico, permitirá que el cuerpo pase, como se muestra aquí.

ingrese la descripción de la imagen aquí

Esto se puede resolver combinando con la Solución 1 .

Una última nota aquí, es posible aplicar esto solo a ciertas interacciones (por ejemplo, aquellas entre un cuerpo estático y no estático). Hacerlo se logra cambiando

if (bodies[i] != dragBody && Matter.SAT.collides(bodies[i], dragBody).collided == true) {
    //...
}

a (por ejemplo, cuerpos estáticos)

if (bodies[i].isStatic == true && bodies[i] != dragBody && 
    Matter.SAT.collides(bodies[i], dragBody).collided == true) {
    //...
}

Soluciones fallidas

En caso de que algún usuario futuro se encuentre con esta pregunta y encuentre que ambas soluciones son insuficientes para su caso de uso, estas son algunas de las soluciones que intenté que no funcionaron. Una guía de lo que no se debe hacer.

  • Llamando mouse.mouseupdirectamente: objeto eliminado de inmediato.
  • Llamando a mouse.mouseuptravés de Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}): anulado por Engine.update, comportamiento sin cambios.
  • Hacer que el objeto arrastrado sea temporalmente estático: objeto eliminado al volver a no estático (ya sea a través de Matter.Body.setStatic(body, false)o body.isStatic = false).
  • Establecer la fuerza a (0,0)través setForcecuando se acerca al conflicto: el objeto aún puede pasar, necesitaría implementarse Resolverpara que realmente funcione.
  • Cambiar mouse.elementa un lienzo diferente a través setElement()o mutando mouse.elementdirectamente: objeto eliminado de inmediato.
  • Revertir el objeto a la última posición 'válida': todavía permite pasar,
  • Cambiar el comportamiento a través de collisionStart: la detección de colisión inconsistente todavía permite pasar con este método

William Miller
fuente
Muchas gracias por tus aportes! Le otorgué la recompensa porque, aunque su solución no fue perfecta, definitivamente señala el camino a seguir y usted puso una gran cantidad de pensamiento y tiempo en este problema. ¡Gracias! Ahora estoy seguro de que este problema es, en última instancia, una brecha de características en MatterJS, y espero que esta discusión contribuya a una solución real en el futuro.
d13
@ d13 Gracias, estoy de acuerdo en que el problema está en el código subyacente, pero me alegra haber podido encontrar alguna solución (s)
William Miller
0

Hubiera gestionado la función de otra manera:

  • Sin "arrastre" (por lo que no hay alineación continua del punto de arrastre con el desplazamiento del objeto arrastrado)
  • En mouseDown, la posición del puntero del mouse proporciona un vector de velocidad orientado para que el objeto siga
  • En mouseUp restablezca su vector de velocidad
  • Deja que la simulación de la materia haga el resto
Mosè Raguzzini
fuente
1
¿No es así como matter.jsmaneja arrastrar cuerpos ya? desde aquí "... como un resorte virtual que se adhiere al mouse. Al arrastrar ... el resorte se une [al cuerpo] y tira en la dirección del mouse ..."
Fantasma
Establecer solo la velocidad evita la superposición de arrastre, el sping fuerza al cuerpo a través de otros.
Mosè Raguzzini
Esto realmente podría apuntar a una solución. Si lo entiendo correctamente, significa no usar MatterJS's incorporado en MouseConstraint y establecer la velocidad del cuerpo manualmente en función de la posición del mouse. Sin embargo, no estoy seguro de cómo se implementaría esto, así que si alguien puede publicar detalles sobre cómo alinear el cuerpo a la posición del mouse, sin usar setPosition o una restricción, por favor hágalo.
d13
@ d13 seguiría confiando en MatterJS Resolverpara decidir qué hacer con los cuerpos en colisión; después de haber examinado ese código un poco, espero que todavía decida permitir el arrastre en muchas circunstancias ... podría funcionar si usted también implementó su propia versión de solveVelocityy, solvePositionpero en ese punto todavía está haciendo manualmente lo que desea que MatterJS maneje directamente ...
Ghost
0

Para controlar la colisión cuando se arrastra, debe utilizar el filtro de colisión y los eventos .

Cree cuerpos con la máscara de filtro de colisión predeterminada 0x0001. Agregue capturas startdragy enddrageventos y establezca diferentes categorías de filtro de colisión corporal para evitar temporalmente colisiones.

Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
    event.body.collisionFilter.category = 0x0008; // move body to new category to avoid collision
});
Matter.Events.on(mouseConstraint, 'enddrag', function(event) {
     event.body.collisionFilter.category = 0x0001; // return body to default category to activate collision
});

window.addEventListener('load', function () {

  //Fetch our canvas
  var canvas = document.getElementById('world');

  //Setup Matter JS
  var engine = Matter.Engine.create();
  var world = engine.world;
  var render = Matter.Render.create({
                                      canvas: canvas,
                                      engine: engine,
                                      options: {
                                        width: 800,
                                        height: 800,
                                        background: 'transparent',
                                        wireframes: false,
                                        showAngleIndicator: false
                                      }
                                    });

  //Add a ball
  const size = 50;
  const stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6, 0, 0, (x, y) => {
    return Matter.Bodies.rectangle(x, y, size * 2, size, {
      collisionFilter: {
            mask: 0x0001,
      },
      slop: 0.5,
      friction: 1,
      frictionStatic: Infinity,
    });
  });

  Matter.World.add(engine.world, stack);

  //Add a floor
  var floor = Matter.Bodies.rectangle(250, 520, 500, 40, {
    isStatic: true, //An immovable object
    render: {
      visible: false
    }
  });
  Matter.World.add(world, floor);

  //Make interactive
  var mouseConstraint = Matter.MouseConstraint.create(engine, { //Create Constraint
    element: canvas,

    constraint: {
      render: {
        visible: false
      },
      stiffness: 0.8
    }
  });
  Matter.World.add(world, mouseConstraint);

  // add events to listen drag
  Matter.Events.on(mouseConstraint, 'startdrag', function (event) {
    event.body.collisionFilter.category = 0x0008; // move body to new category to avoid collision
  });
  Matter.Events.on(mouseConstraint, 'enddrag', function (event) {
    event.body.collisionFilter.category = 0x0001; // return body to default category to activate collision
  });

  //Start the engine
  Matter.Engine.run(engine);
  Matter.Render.run(render);

});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.min.js"></script>

Temur Tchanukvadze
fuente
1
Muchas gracias por tu excelente demo! De hecho, estoy tratando de lograr el efecto contrario: necesito evitar que los cuerpos se crucen cuando uno es arrastrado hacia otro.
d13
Lo siento si entendí mal el problema. ¿Puedes aclarar lo que quieres decir al evitar que los cuerpos se crucen? ¿Estás tratando de evitar arrastrar a través de otros objetos cuando se aplica la fuerza?
Temur Tchanukvadze
1
En ese caso, es un problema abierto y no se puede hacer sin codificar para implementar CCD. Echa un vistazo: github.com/liabru/matter-js/issues/5
Temur Tchanukvadze
0

Esto parece estar relacionado con el problema 672 en su página de GitHub, que parece sugerir que esto ocurre debido a la falta de detección continua de colisiones (CCD).

Se ha intentado remediar esto y el código se puede encontrar aquí, pero el problema aún está abierto, por lo que parece que necesitará editar el motor para construir CCD en usted mismo.

Mweya Ruider
fuente
1
¡Gracias por tu respuesta! Lo había considerado pero creo que no es un problema de CCD sino un problema de "¿Qué sucede cuando una fuerza imparable se encuentra con un obstáculo inamovible?" De alguna manera, necesito descubrir cómo neutralizar las fuerzas para evitar que los cuerpos se crucen.
d13