Evitar corpos de força de arrasto através de outros corpos com o MatterJS

14

Estou usando o MatterJs para um jogo baseado em física e não encontrei uma solução para o problema de impedir que os corpos sejam arrastados à força pelo mouse através de outros corpos. Se você arrastar um corpo para outro, o corpo que está sendo arrastado pode forçar-se a entrar e atravessar o outro corpo. Estou procurando uma maneira confiável de impedir que eles se cruzem. Você pode observar esse efeito em qualquer demonstração do MatterJS selecionando um corpo com o mouse e tentando forçá-lo a outro corpo. Aqui está um exemplo típico:

insira a descrição da imagem aqui

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

Infelizmente, isso quebra todos os jogos ou simulações, dependendo do recurso de arrastar e soltar. Tentei várias soluções, como quebrar a restrição do mouse quando ocorre uma colisão ou reduzir a rigidez da restrição, mas nada que funcione de maneira confiável.

Todas as sugestões são bem-vindas!

d13
fonte
Eu não entendo o texto arrastado à força. Você quer dizer que seu corpo arrastado deve passar por outros corpos?
Grdzi 13/12/19
Não, isso significa que o corpo arrastado deve ser impedido de passar por outros corpos.
d13
11
@ d13 Você poderia adicionar uma animação mostrando o problema? Desde parece haver alguma confusão baseada na redacção ...
Santo
2
@Ghost added ...
d13 /
@ d13 que faz mais clara coisas ..... este é um assunto delicado
Santo

Respostas:

6

Penso que a melhor resposta aqui seria uma revisão significativa do Matter.Resolvermódulo para implementar a prevenção preditiva de conflitos físicos entre quaisquer órgãos. É garantido que tudo menos que isso falhe em determinadas circunstâncias. Dito isto, aqui estão duas "soluções" que, na realidade, são apenas soluções parciais. Eles estão descritos abaixo.


Solução 1 (atualização)

Esta solução tem várias vantagens:

  • É mais conciso que a Solução 2
  • Cria uma pegada computacional menor que a Solução 2
  • O comportamento do arrasto não é interrompido da maneira que está na Solução 2
  • Pode ser combinado de maneira não destrutiva com a Solução 2

A idéia por trás dessa abordagem é resolver o paradoxo do que acontece " quando uma força imparável encontra um objeto imóvel ", tornando a força parável. Isso é habilitado pelo Matter.Event beforeUpdate, que permite que a velocidade e o impulso absolutos (ou melhor positionImpulse, o que não é realmente o impulso físico) em cada direção sejam limitados dentro dos limites definidos pelo usuário.

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>

No exemplo que estou restringindo o velocitye positionImpulsena xe ypara a magnitude máxima de 25.0. O resultado é apresentado abaixo

insira a descrição da imagem aqui

Como você pode ver, é possível ser bastante violento ao arrastar os corpos e eles não passarão um pelo outro. É isso que diferencia essa abordagem das outras: a maioria das outras soluções em potencial falha quando o usuário é suficientemente violento com o arrastamento.

A única falha que encontrei com esse método é que é possível usar um corpo não estático para atingir outro corpo não estático com força suficiente para dar velocidade suficiente ao ponto em que o Resolvermódulo falhará ao detectar a colisão e permitir que o segundo corpo a passar por outros corpos. (No exemplo de atrito estático, a velocidade necessária está em torno 50.0, só consegui fazer isso com êxito uma vez e, consequentemente, não tenho uma animação representando).


Solução 2

Esta é uma solução adicional, porém, um aviso justo: não é simples.

Em termos gerais, a maneira como isso funciona é verificar se o corpo que está sendo arrastado dragBodycolidiu com um corpo estático e se o mouse se moveu muito longe sem dragBodysegui-lo. Se detectar que a separação entre o mouse e dragBodyse tornou muito grande, remove o ouvinte de eventos e o substitui por uma função diferente de remoção de mouse ,. Esta função verifica se o mouse retornou a uma determinada proximidade do centro do corpo. Infelizmente, não consegui fazer com que o método interno funcionasse corretamente, por isso tive que incluí-lo diretamente (alguém com mais conhecimento que eu em Javascript precisará descobrir isso). Por fim, se um evento for detectado, ele retornará ao ouvinte 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>

Depois de aplicar o esquema de troca de ouvinte de eventos, os corpos agora se comportam mais como este

insira a descrição da imagem aqui

Eu testei isso bastante bem, mas não posso garantir que funcione em todos os casos. Também é importante notar que o mouseupevento não é detectado, a menos que o mouse esteja dentro da tela quando ocorre - mas isso é verdade para qualquer mouseupdetecção do Matter.js, por isso não tentei corrigir isso.

Se a velocidade for suficientemente grande, Resolverfalhará na detecção de qualquer colisão e, como carece de prevenção preditiva desse sabor de conflito físico, permitirá que o corpo passe, como mostrado aqui.

insira a descrição da imagem aqui

Isso pode ser resolvido combinando-se com a solução 1 .

Uma última observação aqui, é possível aplicar isso apenas a certas interações (por exemplo, entre um corpo estático e um corpo não estático). Fazer isso é conseguido alterando

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

para (por exemplo, corpos estáticos)

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

Soluções com falha

Caso algum usuário futuro se depare com essa questão e encontre as duas soluções insuficientes para o caso de uso, aqui estão algumas das soluções que tentei que não funcionaram. Um guia das sortes para o que não fazer.

  • Chamando mouse.mouseupdiretamente: objeto excluído imediatamente.
  • Chamando mouse.mouseupvia Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}): substituído por Engine.update, comportamento inalterado.
  • Tornando o objeto arrastado temporariamente estático: objeto excluído ao retornar ao não estático (seja via Matter.Body.setStatic(body, false)ou body.isStatic = false).
  • Definir a força como (0,0)via setForceao se aproximar de um conflito: o objeto ainda pode passar, precisaria ser implementado Resolverpara realmente funcionar.
  • Alterando mouse.elementpara uma tela diferente por meio de setElement()ou pela mutação mouse.elementdireta: objeto excluído imediatamente.
  • Revertendo o objeto para a última posição 'válida': ainda permite a passagem,
  • Altere o comportamento via collisionStart: a detecção inconsistente de colisão ainda permite a passagem com esse método

William Miller
fonte
Muito obrigado por suas contribuições! Eu concedi a você a recompensa porque, embora sua solução não fosse perfeita, ela definitivamente indica o caminho a seguir e você dedica uma grande quantidade de tempo e pensamento a esse problema - Obrigado! Agora estou certo de que esse problema é, em última análise, uma lacuna de recursos no MatterJS, e espero que essa discussão contribua para uma solução real no futuro.
d13
@ d13 Graças, concordo que o problema é em última instância no código subjacente, mas eu estou feliz que eu poderia conseguir alguma semelhança da solução (s)
William Miller
0

Eu teria gerenciado o recurso de outra maneira:

  • Sem "arrastar" (portanto, não há alinhamento contínuo do ponto de arrasto com o deslocamento do objeto arrastado)
  • Em mouseDown, a posição do ponteiro do mouse fornece um vetor de velocidade orientado para o objeto seguir
  • No mouse, redefina seu vetor de velocidade
  • Deixe a simulação de matéria fazer o resto
Mosè Raguzzini
fonte
11
Não é assim que matter.jslida com arrastar corpos? a partir daqui "... como uma mola virtual que atribui ao rato Ao arrastar ... a mola é anexado [para o corpo] e puxa na direção do rato ...."
Santo
Definir apenas a velocidade impede a sobreposição de arrastar, o sping força o corpo através dos outros.
Mosè Raguzzini
Isso pode realmente apontar para uma solução. Se bem entendi, significa não usar o MouseConstraint do MatterJS e definir a velocidade do corpo manualmente com base na posição do mouse. No entanto, não sei exatamente como isso seria implementado; portanto, se alguém puder postar detalhes sobre como alinhar o corpo à posição do mouse, sem usar setPosition ou uma restrição, faça.
d13
@ d13 você continuaria confiando no MatterJS Resolverpara decidir o que fazer com a colisão de corpos - tendo examinado um pouco esse código, espero que ele ainda decida permitir o arrastamento sob muitas circunstâncias ..... pode funcionar se você também implementou sua própria versão de solveVelocitye solvePositionmas nesse ponto você ainda está fazendo manualmente o que você quer MatterJS para lidar diretamente ....
Santo
0

Para controlar a colisão quando arrastada, você precisa utilizar eventos e filtro de colisão .

Crie corpos com a máscara de filtro de colisão padrão 0x0001. Adicione capturas startdrage enddrageventos e defina diferentes categorias de filtros de colisão do corpo para evitar temporariamente colisões.

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
fonte
11
Muito obrigado por sua excelente demonstração! Na verdade, estou tentando obter o efeito oposto: preciso impedir que os corpos se cruzem quando um é arrastado para outro.
d13
Desculpe se eu entendi errado o problema. Você pode esclarecer o que quer dizer, impedindo que os corpos se cruzem? Você está tentando evitar arrastar outros objetos quando a força é aplicada?
Temur Tchanukvadze
11
Nesse caso, é uma questão aberta e não pode ser feita sem a codificação embutida para implementar o CCD. Dê uma olhada: github.com/liabru/matter-js/issues/5
Temur Tchanukvadze
0

Isso parece estar relacionado ao problema 672 na página do GitHub, que parece sugerir que isso ocorre devido à falta de detecção contínua de colisão (CCD).

Foi feita uma tentativa de remediar isso, e o código pode ser encontrado aqui, mas o problema ainda está aberto. Parece que você pode precisar editar o mecanismo para criar o CCD por conta própria.

Mweya Ruider
fonte
11
Obrigado pela sua resposta! Eu tinha considerado isso, mas acredito que não é um problema do CCD, mas um problema de "O que acontece quando uma força imparável encontra um obstáculo imóvel?" De alguma forma, preciso descobrir como neutralizar as forças para impedir que os corpos se cruzem.
d13