避免鼠标移动的 QGraphicsItem 形状的碰撞
问题描述
此处提出了一个有趣的讨论,该讨论是关于在 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
anddamping
- Body
mass
andmoment
of inertia - Space
damping
Space.step
time step / how many calls per QTimer timeout- QTimer
interval
相关文章