避免鼠标移动的 QGraphicsItem 形状的碰撞

2022-01-12 00:00:00 python pyqt5 collision pymunk

问题描述

此处提出了一个有趣的讨论,该讨论是关于在 QGraphicsScene 中防止由 QGraphicsEllipseItems 构成的圆的碰撞.这个问题将范围缩小到 2 个碰撞项目,但更大的目标仍然存在,对于任意数量的碰撞怎么办?

An interesting discussion was raised here about preventing collisions of circles, made of QGraphicsEllipseItems, in a QGraphicsScene. The question narrowed the scope to 2 colliding items but the larger goal still remained, what about for any number of collisions?

这是期望的行为:

  • 当一个项目被拖动到其他项目上时,它们不应重叠,而是应该在这些项目周围移动,尽可能靠近鼠标.
  • 如果它被其他物品阻挡,它不应该传送".
  • 应该是平稳且可预测的运动.

随着为圆在移动时找到最佳安全"位置变得越来越复杂,我想展示另一种使用物理模拟器来实现它的方法.

As this becomes increasingly complex to find the best "safe" position for the circle while it’s moving I wanted to present another way to implement this using a physics simulator.


解决方案

鉴于上面描述的行为,它是 2D 刚体物理的一个很好的候选者,也许没有它可以做到,但很难做到完美.我在这个例子中使用 pymunk 因为我很熟悉它,但是相同的概念也适用于其他库.

Given the behavior described above it’s a good candidate for 2D rigid body physics, maybe it can be done without but it would be difficult to get it perfect. I am using pymunk in this example because I’m familiar with it but the same concepts will work with other libraries.

场景有一个运动体来表示鼠标,而圆圈最初由静态体表示.当一个圆圈被选中时,它会切换到一个动态物体,并通过一个阻尼弹簧约束到鼠标.它的位置随着空间在每个超时间隔上按给定时间步更新而更新.

The scene has a kinematic body to represent the mouse and the circles are represented by static bodies initially. While a circle is selected it switches to a dynamic body and is constrained to the mouse by a damped spring. Its position is updated as the space is updated by a given time step on each timeout interval.

该项目实际上并未以与未启用 ItemIsMovable 标志相同的方式移动,这意味着它不再随鼠标立即移动.它非常接近,但有一点延迟,尽管您可能更喜欢这样来更好地了解它对碰撞的反应.(即便如此,您也可以微调参数,让它比我移动得更快/更靠近鼠标**).

The item is not actually moved in the same way as the ItemIsMovable flag is not enabled, which means it no longer moves instantly with the mouse. It’s very close but there’s a small delay, although you may prefer this to better see how it reacts to collisions. (Even so, you can fine-tune the parameters to have it move faster/closer to the mouse than I did**).

另一方面,碰撞得到了完美的处理,并且已经支持其他类型的形状.

On the other hand, the collisions are handled perfectly and will already support other kinds of shapes.

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pymunk

class Circle(QGraphicsEllipseItem):

    def __init__(self, r, **kwargs):
        super().__init__(-r, -r, r * 2, r * 2, **kwargs)
        self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.static = pymunk.Body(body_type=pymunk.Body.STATIC)
        self.circle = pymunk.Circle(self.static, r)
        self.circle.friction = 0
        mass = 10
        self.dynamic = pymunk.Body(mass, pymunk.moment_for_circle(mass, 0, r))
        self.updatePos = lambda: self.setPos(*self.dynamic.position, dset=False)

    def setPos(self, *pos, dset=True):
        super().setPos(*pos)
        if len(pos) == 1:
            pos = pos[0].x(), pos[0].y()
        self.static.position = pos
        if dset:
            self.dynamic.position = pos

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemSelectedChange:
            space = self.circle.space
            space.remove(self.circle.body, self.circle)
            self.circle.body = self.dynamic if value else self.static
            space.add(self.circle.body, self.circle)
        return super().itemChange(change, value)

    def paint(self, painter, option, widget):
        option.state &= ~QStyle.State_Selected
        super().paint(painter, option, widget)


class Scene(QGraphicsScene):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.space = pymunk.Space()
        self.space.damping = 0.02
        self.body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
        self.space.add(self.body)
        self.timer = QTimer(self, timerType=Qt.PreciseTimer, timeout=self.step)
        self.selectionChanged.connect(self.setConstraint)

    def setConstraint(self):
        selected = self.selectedItems()
        if selected:
            shape = selected[0].circle
            if not shape.body.constraints:
                self.space.remove(*self.space.constraints)
                spring = pymunk.DampedSpring(
                    self.body, shape.body, (0, 0), (0, 0),
                    rest_length=0, stiffness=100, damping=10)
                spring.collide_bodies = False
                self.space.add(spring)

    def step(self):
        for i in range(10):
            self.space.step(1 / 30)
        self.selectedItems()[0].updatePos()

    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        if self.selectedItems():
            self.body.position = event.scenePos().x(), event.scenePos().y()
            self.timer.start(1000 / 30)
            
    def mouseMoveEvent(self, event):            
        super().mouseMoveEvent(event)
        if self.selectedItems():
            self.body.position = event.scenePos().x(), event.scenePos().y()
        
    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)
        self.timer.stop()

    def addCircle(self, x, y, radius):
        item = Circle(radius)
        item.setPos(x, y)
        self.addItem(item)
        self.space.add(item.circle.body, item.circle)
        return item


if __name__ == '__main__':
    app = QApplication(sys.argv)
    scene = Scene(0, 0, 1000, 800)
    for i in range(7, 13):
        item = scene.addCircle(150 * (i - 6), 400, i * 5)
        item.setBrush(Qt.GlobalColor(i))    
    view = QGraphicsView(scene, renderHints=QPainter.Antialiasing)
    view.show()
    sys.exit(app.exec_())

**可以调整如下:

  • 弹簧刚度阻尼
  • 身体质量惯性矩
  • 空间阻尼
  • Space.step 时间步长/每个 QTimer 超时的调用次数
  • QTimer 间隔
  • Spring stiffness and damping
  • Body mass and moment of inertia
  • Space damping
  • Space.step time step / how many calls per QTimer timeout
  • QTimer interval

相关文章