Impedisci il trascinamento di corpi attraverso altri corpi con MatterJS


14

Sto usando MatterJs per un gioco basato sulla fisica e non ho trovato una soluzione al problema di prevenire il trascinamento forzato del mouse da parte del mouse attraverso altri corpi. Se trascini un corpo in un altro corpo, il corpo che viene trascinato può forzare se stesso dentro e attraverso l'altro corpo. Sto cercando un modo affidabile per impedire loro di intersecarsi. Puoi osservare questo effetto in qualsiasi demo di MatterJS selezionando un corpo con il mouse e provando a forzarlo attraverso un altro corpo. Ecco un tipico esempio:

inserisci qui la descrizione dell'immagine

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

Purtroppo questo interrompe qualsiasi gioco o simulazione a seconda del trascinamento della selezione. Ho tentato numerose soluzioni, come rompere il vincolo del mouse quando si verifica una collisione o ridurre la rigidità del vincolo, ma nulla che funzioni in modo affidabile.

Eventuali suggerimenti sono benvenuti!


Non capisco le parole trascinate con la forza. Vuoi dire che il tuo corpo trascinato dovrebbe passare attraverso altri corpi?
Grodzi,

No, significa che al corpo trascinato dovrebbe essere impedito di attraversare qualsiasi altro corpo.
d13

1
@ d13 Potresti aggiungere un'animazione che mostra il problema? Dal momento che sembra esserci un po 'di confusione basata sulla formulazione ...
Ghost

2
@Ghost aggiunto ...
d13

@ d13 Questo rende le cose più chiare ..... questa è difficile
Ghost

Risposte:


6

Penso che la migliore risposta qui sarebbe una significativa revisione del Matter.Resolvermodulo per implementare l'evitamento predittivo di conflitti fisici tra qualsiasi corpo. Qualunque cosa al di fuori di questo è garantita per fallire in determinate circostanze. Ciò detto qui sono due "soluzioni" che, in realtà, sono solo soluzioni parziali. Sono descritti di seguito.


Soluzione 1 (aggiornamento)

Questa soluzione presenta numerosi vantaggi:

  • È più conciso della soluzione 2
  • Crea un footprint computazionale inferiore rispetto alla soluzione 2
  • Il comportamento di trascinamento non viene interrotto come nella Soluzione 2
  • Può essere combinato in modo non distruttivo con la Soluzione 2

L'idea alla base di questo approccio è quella di risolvere il paradosso di ciò che accade " quando una forza inarrestabile incontra un oggetto immobile " rendendo la forza bloccabile. Questo è abilitato da Matter.Event beforeUpdate, che consente alla velocità assoluta e all'impulso (o meglio positionImpulse, che non è proprio l'impulso fisico) in ciascuna direzione di essere vincolati entro limiti definiti dall'utente.

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>

Nell'esempio, sto limitando velocitye positionImpulsein xe yad una grandezza massima di 25.0. Il risultato è mostrato sotto

inserisci qui la descrizione dell'immagine

Come puoi vedere, è possibile essere abbastanza violenti nel trascinare i corpi e non si attraverseranno. Questo è ciò che distingue questo approccio dagli altri: la maggior parte delle altre potenziali soluzioni fallisce quando l'utente è sufficientemente violento con il trascinamento.

L'unico difetto che ho riscontrato con questo metodo è che è possibile utilizzare un corpo non statico per colpire un altro corpo non statico abbastanza forte da dargli una velocità sufficiente al punto in cui il Resolvermodulo non riuscirà a rilevare la collisione e consentire al secondo corpo per passare attraverso altri corpi. (Nell'esempio dell'attrito statico la velocità richiesta è intorno 50.0, sono riuscito a farlo con successo solo una volta, e di conseguenza non ho un'animazione che lo rappresenti).


Soluzione 2

Questa è una soluzione aggiuntiva, un avvertimento equo: non è semplice.

In termini generali, il modo in cui funziona è verificare se il corpo trascinato dragBody, si è scontrato con un corpo statico e se il mouse si è spostato troppo lontano senza dragBodyseguirlo. Se rileva che la separazione tra il mouse e dragBodydiventa troppo elevato rimuove l' ascoltatore evento da e lo sostituisce con una funzione diversa mousemove, . Questa funzione controlla se il mouse è tornato in una data vicinanza al centro del corpo. Sfortunatamente non sono riuscito a far funzionare correttamente il metodo integrato, quindi ho dovuto includerlo direttamente (qualcuno più esperto di me in Javascript dovrà capirlo). Infine, se viene rilevato un evento, questo ritorna al normale ascoltatore.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>

Dopo aver applicato lo schema di commutazione del listener di eventi, i corpi ora si comportano in questo modo

inserisci qui la descrizione dell'immagine

L'ho provato abbastanza accuratamente, ma non posso garantire che funzionerà in ogni caso. Vale anche la pena notare che l' mouseupevento non viene rilevato a meno che il mouse non si trovi nell'area di disegno quando si verifica, ma questo è vero per qualsiasi mouseuprilevamento Matter.js, quindi non ho provato a risolverlo.

Se la velocità è sufficientemente grande, Resolvernon riuscirà a rilevare alcuna collisione e, poiché manca di prevenzione predittiva di questo tipo di conflitto fisico, consentirà al corpo di passare, come mostrato qui.

inserisci qui la descrizione dell'immagine

Questo può essere risolto combinandolo con la Soluzione 1 .

Un'ultima nota qui, è possibile applicare questo solo a determinate interazioni (ad esempio quelle tra un corpo statico e un corpo non statico). Ciò si ottiene cambiando

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

a (ad es. corpi statici)

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

Soluzioni fallite

Nel caso in cui eventuali futuri utenti incontrino questa domanda e trovino entrambe le soluzioni insufficienti per il loro caso d'uso, ecco alcune delle soluzioni che ho tentato che non ha funzionato. Una sorta di guida su cosa non fare.

  • Chiamata mouse.mouseupdiretta: oggetto eliminato immediatamente.
  • Chiamata mouse.mouseupvia Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}): sovrascritto da Engine.update, comportamento invariato.
  • Rendere temporaneamente statico l'oggetto trascinato: oggetto eliminato al ritorno su non statico (tramite Matter.Body.setStatic(body, false)o body.isStatic = false).
  • Impostando la forza su (0,0)via setForcequando ci si avvicina al conflitto: l'oggetto può ancora passare attraverso, dovrebbe essere implementato Resolverper funzionare davvero.
  • Passare mouse.elementa una tela diversa tramite setElement()o mutando mouse.elementdirettamente: l'oggetto eliminato immediatamente.
  • Ripristino dell'oggetto nell'ultima posizione "valida": consente comunque il passaggio,
  • Modifica del comportamento tramite collisionStart: il rilevamento incoerente delle collisioni consente comunque di passare attraverso questo metodo


Grazie mille per i tuoi contributi! Ti ho assegnato la generosità perché anche se la tua soluzione non era perfetta, indica sicuramente la strada da percorrere e hai dedicato un sacco di tempo e pensiero a questo problema - Grazie !! Sono ora certo che questo problema è in definitiva un gap di funzionalità in MatterJS, e spero che questa discussione contribuirà a una soluzione reale in futuro.
d13,

@ d13 Grazie, concordo sul fatto che il problema è in definitiva nel codice sottostante, ma sono contento di poter avere una parvenza di soluzione (s)
William Miller,

0

Avrei gestito la funzione in un altro modo:

  • Nessun "trascinamento" (quindi nessun allineamento continuo del punto di trascinamento con l'offset rispetto all'oggetto trascinato)
  • Su mouseDown la posizione del puntatore del mouse fornisce un vettore di velocità orientato per l'oggetto da seguire
  • Al passaggio del mouse ripristina il tuo vettore di velocità
  • Lascia che la simulazione della materia faccia il resto

1
Non è un po 'come matter.jsgestisce già il trascinamento dei corpi? da qui "... come una molla virtuale che si attacca al mouse. Quando si trascina ... la molla è attaccata [al corpo] e tira nella direzione del mouse ..."
Ghost

Impostando solo la velocità si evita il trascinamento della sovrapposizione, la spinta forza il corpo attraverso gli altri.
Mosè Raguzzini,

Questo potrebbe effettivamente indicare una soluzione. Se ho capito bene, significa non usare il MouseConstraint incorporato di MatterJS e impostare manualmente la velocità del corpo in base alla posizione del mouse. Non sono sicuro di come questo sarebbe implementato, quindi, se qualcuno può pubblicare dettagli su come allineare il corpo alla posizione del mouse, senza usare setPosition o un vincolo, per favore.
d13

@d13 continueresti a fare affidamento su MatterJS Resolverper decidere cosa fare in caso di collisione di corpi - dopo aver esaminato quel codice un po 'mi aspetto che continuerebbe a decidere di consentire il trascinamento in molte circostanze ..... potrebbe funzionare se tu ha anche implementato la tua versione di solveVelocitye, solvePositionma a quel punto stai ancora facendo manualmente quello che vuoi che MatterJS gestisca direttamente ....
Ghost

0

Per controllare la collisione quando trascinato è necessario utilizzare il filtro e gli eventi di collisione .

Creare corpi con la maschera filtro di collisione predefinita 0x0001. Aggiungi catture startdraged enddrageventi e imposta una diversa categoria di filtri di collisione del corpo per evitare temporaneamente collisioni.

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>


1
Grazie mille per la tua eccellente demo! In realtà sto cercando di ottenere l'effetto opposto: devo impedire ai corpi di intersecarsi quando uno viene trascinato in un altro.
d13

Scusa se ho frainteso il problema. Puoi chiarire cosa intendi impedendo ai corpi di intersecarsi? Stai cercando di impedire il trascinamento attraverso altri oggetti quando viene applicata la forza?
Temur Tchanukvadze il

1
In tal caso è un problema aperto e non può essere eseguito senza hard-coding per implementare CCD. Dai un'occhiata: github.com/liabru/matter-js/issues/5
Temur Tchanukvadze il

0

Questo sembra essere correlato al problema 672 sulla loro pagina GitHub, che sembra suggerire che ciò si verifica a causa della mancanza di rilevamento continuo delle collisioni (CCD).

È stato effettuato un tentativo di porre rimedio a questo problema e il codice per questo può essere trovato qui ma il problema è ancora aperto, quindi potrebbe essere necessario modificare il motore per creare CCD in esso.


1
Grazie per la tua risposta! Avevo considerato questo, ma credo che non sia un problema CCD ma un problema di "Cosa succede quando una forza inarrestabile incontra un ostacolo immobile?" In qualche modo ho bisogno di capire come neutralizzare le forze per evitare che i corpi si intersecino.
d13
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.