【问题标题】:Prevent force-dragging bodies through other bodies with MatterJS使用 MatterJS 防止强制拖动物体穿过其他物体
【发布时间】:2021-07-19 04:02:18
【问题描述】:

我正在将 MatterJs 用于基于物理的游戏,但尚未找到解决防止身体被鼠标强行拖动到其他身体的问题的解决方案。如果你将一个物体拖入另一个物体,被拖动的物体可以强迫自己进入并穿过另一个物体。我正在寻找一种可靠的方法来防止它们相交。您可以在任何 MatterJS 演示中通过用鼠标选择一个主体并尝试强制它穿过另一个主体来观察这种效果。这是一个典型的例子:

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

不幸的是,这会破坏任何依赖拖放的游戏或模拟。 我尝试了许多解决方案,例如在发生碰撞时打破鼠标约束,或降低约束刚度,但没有任何可靠的方法。

欢迎提出任何建议!

【问题讨论】:

  • 我不明白强制拖动的措辞。你的意思是你拖动的身体应该穿过任何其他身体吗?
  • 不,这意味着应该阻止被拖动的身体穿过任何其他身体。
  • @d13 你能添加一个动画来显示这个问题吗?由于基于措辞似乎有些混乱......
  • @Ghost 添加...
  • @d13 这让事情变得更清楚.....这是一个棘手的问题

标签: javascript physics matter.js


【解决方案1】:

我认为这里最好的答案是对Matter.Resolver 模块进行重大改造,以实现预测性避免任何身体之间的物理冲突。 保证在某些情况下会失败。这里所说的是两个“解决方案”,实际上,它们只是部分解决方案。它们概述如下。


解决方案 1  (更新)

此解决方案有几个优点:

  • 解决方案2更简洁
  • 解决方案 2 相比,它创建的计算占用空间更小
  • 拖动行为不会像 解决方案 2 中那样中断
  • 可以与解决方案2无损结合

这种方法背后的想法是通过使力可停止来解决“当不可阻挡的力量遇到不可移动的物体时”所发生的悖论。这是由Matter.EventbeforeUpdate 启用的,它允许将每个方向上的绝对速度和冲量(或者更确切地说positionImpulse,这不是真正的物理冲量)限制在用户定义的范围内。

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>

在示例中,我将xy 中的velocitypositionImpulse 限制为25.0 的最大值。结果如下图

如您所见,拖动尸体时可能会相当暴力,它们不会相互穿过。这就是这种方法与其他方法的不同之处:当用户拖拽足够暴力时,大多数其他潜在的解决方案都会失败。

我在这种方法中遇到的唯一缺点是可以使用非静态物体撞击另一个非静态物体,使其有足够的速度达到Resolver 模块无法达到的程度检测碰撞并允许第二个物体穿过其他物体。 (在静摩擦示例中,所需的速度约为50.0,我只成功完成了一次,因此我没有动画来描述它。


解决方案 2

这是一个额外的解决方案,但公平警告:它并不简单。

从广义上讲,它的工作方式是检查被拖动的物体 dragBody 是否与静态物体碰撞,以及鼠标是否在没有 dragBody 跟随的情况下移动得太远。如果它检测到鼠标和dragBody 之间的距离太大,它会从mouse.element 中删除Matter.js mouse.mousemove 事件侦听器,并用不同的mousemove 函数mousemove() 替换它。此函数检查鼠标是否已返回到身体中心的给定接近范围内。不幸的是,我无法让内置的 Matter.Mouse._getRelativeMousePosition() 方法正常工作,所以我不得不直接包含它(在 Javascript 中比我更了解的人必须弄清楚这一点)。最后,如果检测到mouseup 事件,它会切换回正常的mousemove 侦听器。

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>

应用事件侦听器切换方案后,主体现在的行为更像这样

我已经对这个相当进行了彻底的测试,但我不能保证它在所有情况下都能正常工作。还值得注意的是,除非鼠标在画布内发生,否则不会检测到 mouseup 事件 - 但对于任何 Matter.js mouseup 检测都是如此,所以我没有尝试修复它。

如果速度足够大,Resolver 将无法检测到任何碰撞,并且由于它缺乏对这种物理冲突味道的预测性预防,将允许身体通过,如下所示。

这可以通过结合解决方案1来解决。

最后一点,可以将其仅应用于某些交互(例如,静态和非静态主体之间的交互)。这样做是通过改变来完成的

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

到(例如静态物体)

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

失败的解决方案

如果未来的用户遇到这个问题并发现这两种解决方案都不足以满足他们的用例,这里有一些我尝试过的解决方案不起作用。关于不该做什么的各种指南。

  • 直接调用mouse.mouseup:对象立即删除。
  • 通过Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}) 调用mouse.mouseup:被Engine.update 覆盖,行为不变。
  • 使拖动的对象暂时静态:对象在返回非静态时被删除(无论是通过Matter.Body.setStatic(body, false) 还是body.isStatic = false)。
  • 在接近冲突时通过setForce 将力设置为(0,0):对象仍然可以通过,需要在Resolver 中实现才能实际工作。
  • 通过setElement() 或通过直接改变mouse.elementmouse.element 更改为不同的画布:立即删除对象。
  • 将对象恢复到上一个​​“有效”位置:仍然允许通过,
  • 通过collisionStart 更改行为:不一致的碰撞检测仍然允许使用此方法通过

【讨论】:

  • 非常感谢您的贡献!我授予您赏金是因为即使您的解决方案并不完美,但它确实指明了前进的方向,并且您在这个问题上投入了大量的思想和时间 - 谢谢!我现在确定这个问题最终是 MatterJS 中的一个功能差距,希望这次讨论将有助于未来的真正解决方案。
  • @d13 谢谢,我同意问题最终出在底层代码中,但我很高兴我能得到一些解决方案
  • 这个方法在matter.js版本0.12.0之后停止工作
【解决方案2】:

我会以另一种方式管理该功能:

  • 没有“拖动”(因此拖动点没有连续对齐偏移与拖动对象)
  • 在 mouseDown 上,鼠标指针位置为要跟随的对象提供定向速度矢量
  • 在 mouseUp 上重置您的速度矢量
  • 让物质模拟完成剩下的工作

【讨论】:

  • 这不是matter.js 处理拖拽物体的方式吗?来自here "...就像一个虚拟弹簧附在鼠标上。拖动时...弹簧附在[身体]上并拉向鼠标的方向..."
  • 只设置速度可以防止拖动重叠,sping 迫使身体穿过其他物体。
  • 这实际上可能指向一个解决方案。如果我理解正确,这意味着不使用 MatterJS 内置的 MouseConstraint 并根据鼠标的位置手动设置身体的速度。但是,我不确定这将如何实现,因此,如果有人可以发布有关如何在不使用 setPosition 或约束的情况下将主体与鼠标位置对齐的详细信息,请执行。
  • @d13 你仍然依赖于 MatterJS 的 Resolver 来决定如何处理碰撞体 - 仔细查看了该代码,我希望它仍然会决定允许在下拖动许多情况.....如果您还实现了自己的solveVelocitysolvePosition 版本,可能会起作用,但此时您仍然需要手动执行您希望MatterJS 直接处理的操作....
【解决方案3】:

要控制拖动时的碰撞,您需要使用collision filterevents

使用默认 collision filter mask 0x0001 创建实体。添加 catch startdragenddrag 事件并设置不同的 body collision filter category 以暂时避免碰撞。

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>

【讨论】:

  • 非常感谢您的精彩演示!我实际上是在试图达到相反的效果:当一个物体被拖入另一个物体时,我需要防止物体相交。
  • 对不起,如果我误解了这个问题。你能澄清一下防止物体相交是什么意思吗?您是否试图防止在施加力时拖过其他物体?
  • 在这种情况下,这是一个未解决的问题,如果没有硬编码来实现 CCD,就无法完成。看一看:github.com/liabru/matter-js/issues/5
【解决方案4】:

这似乎与他们 GitHub 页面上的 issue 672 相关,这似乎表明这是由于缺乏连续碰撞检测 (CCD) 而导致的。

已尝试解决此问题,可以在 here 找到它的代码,但问题仍然存在,因此您可能需要编辑引擎以自己构建 CCD。

【讨论】:

  • 感谢您的回答!我曾考虑过这一点,但我认为这不是 CCD 问题,而是“当不可阻挡的力量遇到不可移动的障碍时会发生什么?”的问题。不知何故,我需要弄清楚如何抵消这些力量以防止身体相交。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-01
  • 1970-01-01
  • 2014-03-07
  • 1970-01-01
相关资源
最近更新 更多