Zapobiegaj przeciąganiu ciał przez inne ciała za pomocą MatterJS

14

Używam MatterJs do gry opartej na fizyce i nie znalazłem rozwiązania problemu zapobiegania przeciąganiu ciał przez mysz przez inne ciała. Jeśli przeciągniesz ciało do innego ciała, przeciągane ciało może wtłoczyć się do i przez inne ciało. Szukam niezawodnego sposobu, aby zapobiec ich przecięciu. Możesz zaobserwować ten efekt w dowolnej wersji demonstracyjnej MatterJS, wybierając ciało myszką i próbując przepchnąć je przez inne ciało. Oto typowy przykład:

wprowadź opis zdjęcia tutaj

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

Niestety łamie to wszelkie gry lub symulacje w zależności od przeciągania i upuszczania. Próbowałem wielu rozwiązań, takich jak przełamanie ograniczenia myszy w przypadku kolizji lub zmniejszenie sztywności ograniczenia, ale nic, co działa niezawodnie.

Wszelkie sugestie mile widziane!

d13
źródło
Nie rozumiem przeciągniętego sformułowania. Czy masz na myśli, że twoje przeciągnięte ciało powinno przejść przez inne ciała?
grodzi
Nie, oznacza to, że przeciągnięte ciało nie powinno przechodzić przez inne ciała.
d13
1
@ d13 Czy możesz dodać animację przedstawiającą problem? Ponieważ wydaje się, że istnieje pewne zamieszanie oparte na sformułowaniu ...
Ghost
2
@Ghost dodaje ...
d13,
@ d13 To sprawia, że ​​wszystko staje się jaśniejsze ..... to jest trudne
Ghost

Odpowiedzi:

6

Myślę, że najlepszą odpowiedzią na to pytanie będzie znaczący przegląd Matter.Resolvermodułu w celu wdrożenia predykcyjnego unikania fizycznych konfliktów między dowolnymi ciałami. Jest coś brakuje, że gwarantowane niepowodzenie w pewnych okolicznościach. Mówi się tutaj o dwóch „rozwiązaniach”, które w rzeczywistości są tylko rozwiązaniami częściowymi. Są one przedstawione poniżej.


Rozwiązanie 1 (aktualizacja)

To rozwiązanie ma kilka zalet:

  • Jest bardziej zwięzły niż Rozwiązanie 2
  • Tworzy to mniejszą powierzchnię obliczeniową niż Rozwiązanie 2
  • Przeciąganie nie jest przerywane tak, jak w rozwiązaniu 2
  • Może być nieniszczący w połączeniu z rozwiązaniem 2

Ideą tego podejścia jest rozwiązanie paradoksu tego, co dzieje się „ kiedy siła nie do powstrzymania napotyka nieruchomy obiekt ” poprzez uczynienie siły możliwą do zatrzymania. Jest to możliwe dzięki temu Matter.Event beforeUpdate, który pozwala na ograniczenie bezwzględnej prędkości i impulsu (a raczej positionImpulse, który tak naprawdę nie jest impulsem fizycznym) w każdym kierunku w granicach określonych przez użytkownika.

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>

Na przykład ja jestem jego ograniczania velocityi positionImpulsew xi ydo maksymalnej wielkości 25.0. Wynik pokazano poniżej

wprowadź opis zdjęcia tutaj

Jak widać, przeciąganie ciał może być dość gwałtowne i nie będą się one przenikać. Oto, co wyróżnia to podejście od innych: większość innych potencjalnych rozwiązań zawodzi, gdy użytkownik jest wystarczająco gwałtowny podczas przeciągania.

Jedyną wadą, jaką napotkałem przy tej metodzie, jest to, że można użyć ciała niestatycznego, aby uderzyć inne ciało niestatyczne wystarczająco mocno, aby zapewnić mu wystarczającą prędkość do punktu, w którym Resolvermoduł nie wykryje kolizji i pozwoli drugie ciało, aby przejść przez inne ciała. (W przykładzie tarcia statycznego jest wymagana wymagana prędkość 50.0, udało mi się to zrobić tylko raz, a zatem nie mam animacji przedstawiającej to).


Rozwiązanie 2

Jest to dodatkowe rozwiązanie, jednak uczciwe ostrzeżenie: nie jest proste.

Mówiąc ogólnie, sposób ten polega na sprawdzeniu, czy przeciągane ciało dragBodynie zderzyło się ze statycznym ciałem i czy myszka odtąd zbyt daleko się posunęła, nie dragBodypodążając za nim. Jeśli wykryje, że rozdzielenie między myszą a dragBodystał się zbyt duży usuwa detektor zdarzeń z i zastępuje ją inną funkcją mousemove, . Ta funkcja sprawdza, czy mysz powróciła do określonej odległości od środka ciała. Niestety nie udało mi się sprawić, by wbudowana metoda działała poprawnie, musiałem więc uwzględnić ją bezpośrednio (ktoś bardziej obeznany z JavaScriptem będzie musiał to rozgryźć). Wreszcie, jeśli zostanie wykryte zdarzenie, przełącza się z powrotem na normalny odbiornik.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>

Po zastosowaniu schematu przełączania nasłuchiwania zdarzeń ciała zachowują się teraz mniej więcej tak

wprowadź opis zdjęcia tutaj

Przetestowałem to dość dokładnie, ale nie mogę zagwarantować, że zadziała w każdym przypadku. Należy również zauważyć, że mouseupzdarzenie nie jest wykrywane, chyba że mysz znajdzie się w obszarze roboczym, gdy wystąpi - ale dotyczy to każdego mouseupwykrycia Matter.js, więc nie próbowałem tego naprawić.

Jeśli prędkość jest wystarczająco duża, Resolvernie wykryje żadnej kolizji, a ponieważ nie ma predykcyjnego zapobiegania temu smakowi fizycznego konfliktu, pozwoli ciału przejść, jak pokazano tutaj.

wprowadź opis zdjęcia tutaj

Można to rozwiązać, łącząc się z rozwiązaniem 1 .

I ostatnia uwaga: można to zastosować tylko do niektórych interakcji (np. Między ciałem statycznym i niestatycznym). Robi się to poprzez zmianę

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

do (np. ciał statycznych)

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

Nieudane rozwiązania

W przypadku, gdy przyszli użytkownicy napotkają to pytanie i stwierdzą, że oba rozwiązania są niewystarczające dla ich przypadku użycia, oto niektóre rozwiązania, które próbowałem, które nie działały. Przewodnik po tym, czego nie robić.

  • mouse.mouseupBezpośrednie wywołanie : obiekt natychmiast usunięty.
  • Dzwonienie mouse.mouseupprzez Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}): zastąpione przez Engine.update, zachowanie niezmienione.
  • Uczynienie przeciągniętego obiektu tymczasowym statycznym: obiekt usuwany po powrocie do niestatycznego (za pośrednictwem Matter.Body.setStatic(body, false)lub body.isStatic = false).
  • Ustawianie siły (0,0)poprzez setForcekiedy zbliża się konflikt: obiekt może nadal przechodzą, będą musiały być wdrożone w Resolvercelu faktycznie pracują.
  • Zmiana mouse.elementna inne płótno poprzez setElement()lub poprzez mouse.elementbezpośrednią mutację : obiekt natychmiast usuwany.
  • Cofanie obiektu do ostatniej „prawidłowej” pozycji: nadal umożliwia przejście,
  • Zmień zachowanie poprzez collisionStart: niespójne wykrywanie kolizji nadal pozwala przejść przez tę metodę

William Miller
źródło
Dziękuję bardzo za Twój wkład! Przyznałem ci nagrodę, ponieważ mimo że twoje rozwiązanie nie było idealne, zdecydowanie wskazuje drogę naprzód, a ty poświęcasz temu problemowi wiele uwagi i czasu - dziękuję !! Jestem teraz pewien, że ten problem jest ostatecznie luką w MatterJS i mam nadzieję, że ta dyskusja przyczyni się do prawdziwego rozwiązania w przyszłości.
d13
@ d13 Dzięki, zgadzam się, że problem jest ostatecznie związany z podstawowym kodem, ale cieszę się, że mogłem uzyskać pozory rozwiązania (ów)
William Miller,
0

Zarządzałbym tą funkcją w inny sposób:

  • Brak „przeciągania” (więc brak ciągłego wyrównania punktu przeciągnięcia z przesuniętym obiektem przeciągniętym Vs)
  • Na mouseDown pozycja wskaźnika myszy daje zorientowany wektor prędkości dla obiektu, który ma podążać
  • Na mouseUp zresetuj wektor prędkości
  • Niech symulacja materii zajmie się resztą
Mosè Raguzzini
źródło
1
Czy to już nie tak matter.jsradzi sobie z przeciąganiem ciał? z o „... jak wirtualnego sprężyny, która wiąże z myszy Podczas przeciągania ... sprężyna jest dołączone [w organizmie] i ciągnie w kierunku myszy ...”.
widmo
Ustawienie tylko prędkości zapobiega nakładaniu się przeciągania, sprężyna przepycha ciało przez inne.
Mosè Raguzzini,
To może faktycznie wskazywać na rozwiązanie. Jeśli dobrze rozumiem, oznacza to, że nie korzystam z wbudowanej funkcji MouseConstraint MatterJS i ręcznie ustawiasz prędkość ciała na podstawie położenia myszy. Nie jestem jednak pewien, w jaki sposób zostanie to zaimplementowane, więc jeśli ktoś może opublikować szczegółowe informacje na temat wyrównania ciała do pozycji myszy, bez użycia setPosition lub ograniczenia, zrób to.
d13
@ d13 nadal będziesz polegać na MatterJS Resolver aby zdecydować, co zrobić z kolidującymi ciałami - po przejrzeniu tego kodu spodziewam się, że w wielu okolicznościach nadal zdecydowałby się na przeciąganie przez ... może działać, jeśli również realizowane własną wersję solveVelocity, a solvePositionjednak w tym momencie jesteś jeszcze ręcznie robić to, co chcesz MatterJS obsługiwać bezpośrednio ....
Duch
0

Aby kontrolować kolizję podczas przeciągania, należy użyć filtra kolizji i zdarzeń .

Utwórz ciała z domyślną maską filtra kolizyjnego 0x0001 . Dodaj połów startdragienddrag zdarzenia oraz ustaw inną kategorię filtrów kolizji, aby tymczasowo uniknąć kolizji.

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
źródło
1
Dziękuję bardzo za doskonałą prezentację! Właściwie staram się osiągnąć odwrotny efekt: muszę zapobiec przecinaniu się ciał, gdy jedno zostanie przeciągnięte do drugiego.
d13
Przepraszam, jeśli źle zrozumiałem problem. Czy potrafisz wyjaśnić, co masz na myśli, zapobiegając przecinaniu się ciał? Czy próbujesz zapobiec przeciąganiu przez inne obiekty po przyłożeniu siły?
Temur Tchanukvadze,
1
W takim przypadku jest to kwestia otwarta i nie można tego zrobić bez twardego kodowania implementacji CCD. Spójrz: github.com/liabru/matter-js/issues/5
Temur Tchanukvadze
0

Wydaje się, że jest to związane z problemem 672 na stronie GitHub, który sugeruje, że dzieje się tak z powodu braku ciągłego wykrywania kolizji (CCD).

Podjęto próbę rozwiązania tego problemu, a kod można znaleźć tutaj, ale problem jest nadal otwarty, więc wygląda na to, że być może będziesz musiał edytować silnik, aby sam w nim wbudować CCD.

Mweya Ruider
źródło
1
Dzięki za odpowiedź! Zastanawiałem się nad tym, ale uważam, że nie jest to problem CCD, ale problem „Co się stanie, gdy siła nie do powstrzymania napotka nieporuszalną przeszkodę?” Jakoś muszę wymyślić, jak zneutralizować siły, aby zapobiec krzyżowaniu się ciał.
d13