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

14

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 vers 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émonstration de MatterJS en sélectionnant un corps avec la souris et en essayant de le forcer à travers un autre corps. Voici un exemple typique:

entrez la description de l'image ici

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 lors d'une collision ou réduire la rigidité de la contrainte, mais rien qui fonctionne de manière fiable.

Toutes les suggestions sont les bienvenues!

d13
la source
Je ne comprends pas le libellé forcé. Voulez-vous dire que votre corps traîné devrait traverser d'autres corps?
grodzi
Non, cela signifie que le corps traîné devrait être empêché de passer par tout autre corps.
d13
1
@ d13 Pourriez-vous ajouter une animation montrant le problème? Puisqu'il semble y avoir une certaine confusion basée sur le libellé ...
Ghost
2
@Ghost ajouté ...
d13
@ d13 Cela rend les choses plus claires ... c'est difficile
Ghost

Réponses:

6

Je pense que la meilleure réponse ici serait une refonte significative du Matter.Resolvermodule pour implémenter l'évitement prédictif des conflits physiques entre tous les corps. Tout ce qui est en deçà est garanti d'é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:

  • Il est plus concis que solution 2
  • Il crée une empreinte de calcul plus petite que solution 2
  • Le comportement de glissement n'est pas interrompu comme il est dans solution 2
  • Il peut être combiné de manière non destructive avec la solution 2

L'idée derrière cette approche est de résoudre le paradoxe de ce qui se passe « lorsqu'une force imparable rencontre un objet inamovible » en rendant la force imparable. Ceci est activé par le Matter.Event beforeUpdate, qui permet de positionImpulsecontraindre la vitesse absolue et l'impulsion (ou plutôt , ce qui n'est pas vraiment une impulsion physique) dans chaque direction à l'intérieur de 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 le velocityet positionImpulsedans xet yà une amplitude maximale de 25.0. Le résultat est montré ci-dessous

entrez la description de l'image ici

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 Resolvermodule ne parviendra pas à détecter la collision et à permettre la deuxième corps à traverser d'autres corps. (Dans l'exemple de friction 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 juste: ce n'est pas simple.

En termes généraux, la façon dont cela fonctionne consiste à vérifier si le corps en train d'être glissé, dragBodyest entré en collision avec un corps statique et si la souris s'est depuis déplacée trop loin sans dragBodysuivre. Si elle détecte que la séparation entre la souris et dragBodyest devenu trop grand , il supprime l' écouteur d'événement de et la remplace par une fonction mousemove différente, . 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 intégrée , j'ai donc dû l'inclure directement (quelqu'un de plus compétent que moi en Javascript devra le comprendre). Enfin, si un événement est détecté, il revient à l' écouteur 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>

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

entrez la description de l'image ici

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

Si la vitesse est suffisamment grande, Resolverelle ne détectera aucune collision et, comme elle n'a pas de prévention prédictive de cette saveur de conflit physique, elle permettra au corps de passer, comme illustré ici.

entrez la description de l'image ici

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

Une dernière remarque ici, il est possible de ne l'appliquer qu'à 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 pour les corps statiques)

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

Solutions échouées

Au 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 qui n'ont pas fonctionné. Un guide en quelque sorte pour ne pas faire.

  • Appel mouse.mouseupdirect: objet supprimé immédiatement.
  • Appel mouse.mouseupvia 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 (via Matter.Body.setStatic(body, false)ou body.isStatic = false).
  • Régler la force sur (0,0)viasetForce à l'approche d'un conflit: l'objet peut toujours passer, devrait être implémenté dansResolver pour fonctionner réellement.
  • Passer mouse.elementà une autre toile via setElement()ou par mutationmouse.element directement: objet supprimé immédiatement.
  • Revenir à la dernière position «valide» de l'objet: autorise toujours le passage,
  • Changer de comportement via collisionStart: la détection de collision incohérente permet toujours de passer avec cette méthode

William Miller
la source
Merci beaucoup pour vos contributions! Je vous ai attribué la prime parce que même si votre solution n'était pas parfaite, elle indique certainement la voie à suivre et vous avez consacré énormément de temps et de réflexion à ce problème - Merci !! Je suis maintenant certain que ce problème est finalement une lacune dans MatterJS, et j'espère que cette discussion contribuera à une véritable solution à l'avenir.
d13
@ d13 Merci, je suis d'accord que le problème est finalement dans le code sous-jacent mais je suis heureux d'avoir pu obtenir un semblant de solution (s)
William Miller
0

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
Mosè Raguzzini
la source
1
N'est-ce pas un peu la façon dont matter.jsles corps glissent déjà? d' ici "... comme un ressort virtuel qui s'attache à la souris. Lorsque vous faites glisser ... le ressort est attaché [au corps] et tire dans le sens de la souris ..."
Ghost
Régler uniquement la vélocité empêche le chevauchement de la traînée, la flèche force le corps à travers les autres.
Mosè Raguzzini
Cela pourrait en fait indiquer une solution. Si je comprends bien, cela signifie ne pas utiliser la souris MouseConstraint intégrée à MatterJS et définir manuellement la vitesse du corps en fonction de la position de la souris. Je ne sais pas exactement comment cela serait implémenté, cependant, donc si quelqu'un peut publier des détails sur la façon d'aligner le corps à la position de la souris, sans utiliser setPosition ou une contrainte, veuillez le faire.
d13
@ d13 vous comptez toujours sur MatterJS Resolverpour décider quoi faire à propos des corps en collision - après avoir parcouru ce code un peu, je m'attends à ce qu'il décide toujours d'autoriser le drag-through dans de nombreuses circonstances ..... pourrait fonctionner si vous a également implémenté votre propre version de solveVelocityet solvePositionmais à ce stade, vous faites toujours manuellement ce que vous voulez que MatterJS gère directement ....
Ghost
0

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

Créez des corps avec le masque de filtre de collision par défaut 0x0001. Ajoutez des captures startdraget des enddragévénements et définissez une catégorie de filtre de collision corporelle différente 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>

Temur Tchanukvadze
la source
1
Merci beaucoup pour votre excellente démo! J'essaie en fait d'obtenir l'effet inverse: je dois empêcher les corps de se croiser lorsque l'un est traîné dans un autre.
d13
Désolé si j'ai mal compris le problème. Pouvez-vous clarifier ce que vous voulez dire en empêchant les corps de se croiser? Essayez-vous d'empêcher de glisser à travers d'autres objets lorsque la force est appliquée?
Temur Tchanukvadze
1
Dans ce cas, c'est un problème ouvert et ne peut pas être fait sans codage en dur pour implémenter CCD. Jetez un coup d'œil: github.com/liabru/matter-js/issues/5
Temur Tchanukvadze
0

Cela semble être lié au 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 devrez peut-être modifier le moteur pour y intégrer vous-même CCD.

Mweya Ruider
la source
1
Merci pour votre réponse! J'avais pensé à cela mais je crois que ce n'est pas un problème CCD mais un problème de "Que se passe-t-il quand une force imparable rencontre un obstacle inamovible?" D'une certaine manière, je dois trouver un moyen de neutraliser les forces pour empêcher les corps de se croiser.
d13