web-dev-qa-db-fra.com

Empêchez les corps de glisser à travers d'autres corps avec MatterJS

J'utilise MatterJs pour un jeu basé sur la physique et je n'ai pas trouvé de solution au problème d'empêcher les corps d'être traînés de force par la souris à travers d'autres corps. Si vous faites glisser un corps dans un autre corps, le corps que vous faites glisser peut se forcer dans et à travers l'autre corps. Je cherche un moyen fiable de les empêcher de se croiser. Vous pouvez observer cet effet dans n'importe quelle démo MatterJS en sélectionnant un corps avec la souris et en essayant de le forcer à travers un autre corps. Voici un exemple typique:

enter image description here

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

Malheureusement, cela interrompt tous les jeux ou simulations en fonction du glisser-déposer. J'ai tenté de nombreuses solutions, comme briser la contrainte de la souris en cas de collision ou réduire la rigidité de la contrainte, mais rien qui fonctionne de manière fiable.

Toutes les suggestions sont les bienvenues!

14
d13

Je pense que la meilleure réponse ici serait une refonte importante du module _Matter.Resolver_ pour implémenter l'évitement prédictif des conflits physiques entre tous les corps. Tout ce qui est court est garanti pour échouer dans certaines circonstances. Cela étant dit, ce sont deux "solutions" qui, en réalité, ne sont que des solutions partielles. Ils sont décrits ci-dessous.


Solution 1 (mise à jour)

Cette solution présente plusieurs avantages:

  • Elle est plus concise que Solution 2
  • Il crée une empreinte de calcul plus petite que Solution 2
  • Le comportement de glissement n'est pas interrompu tel qu'il est dans Solution 2
  • Il peut être combiné de manière non destructive avec Solution 2

L'idée derrière cette approche est de résoudre le paradoxe de ce qui se passe " quand une force imparable rencontre un objet inamovible" en rendant la force stoppable. Ceci est activé par le _Matter.Event_ beforeUpdate, qui permet de contraindre la vitesse absolue et l'impulsion (ou plutôt positionImpulse, qui n'est pas vraiment une impulsion physique) dans chaque direction. limites définies par l'utilisateur.

_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>_

Dans l'exemple, je limite les velocity et positionImpulse dans x et y à une magnitude maximale de _25.0_. Le résultat est montré ci-dessous

enter image description here

Comme vous pouvez le voir, il est possible d'être assez violent en traînant les corps et ils ne passeront pas entre eux. C'est ce qui distingue cette approche des autres: la plupart des autres solutions potentielles échouent lorsque l'utilisateur est suffisamment violent avec son glissement.

Le seul inconvénient que j'ai rencontré avec cette méthode est qu'il est possible d'utiliser un corps non statique pour frapper un autre corps non statique suffisamment fort pour lui donner une vitesse suffisante au point où le module Resolver ne pourra pas détecter la collision et permettre au deuxième corps de passer à travers d'autres corps. (Dans l'exemple de frottement statique, la vitesse requise est d'environ _50.0_, je n'ai réussi à le faire qu'une fois avec succès, et par conséquent je n'ai pas d'animation le représentant).


Solution 2

Il s'agit d'une solution supplémentaire, avertissement cependant: ce n'est pas simple.

En termes généraux, la façon dont cela fonctionne consiste à vérifier si le corps que vous faites glisser, dragBody, est entré en collision avec un corps statique et si la souris s'est depuis déplacée trop loin sans que dragBody ne le suive. S'il détecte que la séparation entre la souris et dragBody est devenue trop importante, il supprime l'écouteur d'événement _Matter.js_ _mouse.mousemove_ de _mouse.element_ et remplace avec une fonction de déplacement de souris différente, mousemove(). Cette fonction vérifie si la souris est revenue à une certaine proximité du centre du corps. Malheureusement, je n'ai pas pu faire fonctionner correctement la méthode Matter.Mouse._getRelativeMousePosition() intégrée, j'ai donc dû l'inclure directement (quelqu'un de plus compétent que moi en Javascript devra trouver celle-là). Enfin, si un événement mouseup est détecté, il revient à l'écouteur mousemove normal.

_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>_

Après avoir appliqué le schéma de commutation de l'écouteur d'événements, les corps se comportent maintenant comme ceci

enter image description here

J'ai testé ce assez à fond, mais je ne peux pas garantir que cela fonctionnera dans tous les cas. Il convient également de noter que l'événement mouseup n'est pas détecté à moins que la souris ne se trouve dans le canevas lorsqu'il se produit - mais cela est vrai pour toute détection Matter.js mouseup, donc je n'ai pas essayé de répare ça.

Si la vitesse est suffisamment grande, Resolver ne détectera aucune collision et, comme il manque de prévention prédictive de cette saveur de conflit physique, permettra au corps de passer, comme illustré ici.

enter image description here

Cela peut être résolu en combinant avec la solution 1 .

Une dernière remarque ici, il est possible de l'appliquer uniquement à certaines interactions (par exemple celles entre un corps statique et un corps non statique). Pour ce faire, il faut changer

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

à (par exemple des corps statiques)

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

Solutions échouées

Dans le cas où de futurs utilisateurs rencontreraient cette question et trouveraient les deux solutions insuffisantes pour leur cas d'utilisation, voici quelques-unes des solutions que j'ai essayées et qui n'ont pas fonctionné. Un guide en quelque sorte pour ne pas faire.

  • Appel de _mouse.mouseup_ directement: objet supprimé immédiatement.
  • Appel de _mouse.mouseup_ via Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}): remplacé par _Engine.update_, comportement inchangé.
  • Rendre l'objet glissé temporairement statique: objet supprimé lors du retour à non statique (que ce soit via Matter.Body.setStatic(body, false) ou _body.isStatic = false_).
  • Définir la force sur _(0,0)_ via setForce à l'approche d'un conflit: l'objet peut toujours passer, devrait être implémenté dans Resolver pour fonctionner réellement.
  • Changement de _mouse.element_ vers un canevas différent via setElement() ou en mutant _mouse.element_ directement: objet supprimé immédiatement.
  • Retour de l'objet à la dernière position "valide": permet toujours le passage,
  • Modifier le comportement via collisionStart: la détection de collision incohérente permet toujours de passer avec cette méthode

6
William Miller

Cela semble être lié à problème 672 sur leur page GitHub qui semble suggérer que cela se produit en raison d'un manque de détection de collision continue (CCD).

Une tentative de remédier à cela a été faite et le code pour cela peut être trouvé ici mais le problème est toujours ouvert, il semble que vous ayez besoin de modifier le moteur pour y intégrer vous-même CCD.

0
Mweya Ruider

Pour contrôler les collisions lorsque vous les faites glisser, vous devez utiliser filtre de collision et événements .

Créer des corps avec par défaut masque de filtre de collision0x0001. Ajoutez des événements catch startdrag et enddrag et définissez un corps différent catégorie de filtre de collision pour éviter temporairement les collisions.

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>
0
Temur Tchanukvadze

J'aurais géré la fonctionnalité d'une autre manière:

  • Pas de "glisser" (donc pas d'alignement continu du point de glissement avec l'objet déplacé Vs décalé)
  • On mouseDown La position du pointeur de la souris donne un vecteur de vitesse orienté pour l'objet à suivre
  • Sur mouseUp réinitialiser votre vecteur de vitesse
  • Laissez la simulation de matière faire le reste
0
Mosè Raguzzini