I'm trying to make a moving bullet in Tkinter on a canvas. I delete it and redraw it every frame. The canvas still gets slow after only around 4/5 drawings. What am I missing?
class Bullet:
def __init__(self,x,y,r,vx,vy):
# realX/Y represent the real location of the bullet as a float
self.realX = x
self.realY = y
self.vx = vx
self.vy = vy
self.r = r # Radius
self.speed = 0.96 # Pixels per frame
def level1_loop():
move_bullets()
draw()
window.after(1,level1_loop)
def move_bullets():
global timer
global canvas
if time() > timer + 1:
# Create a new bullet every second
newbul = Bullet(rand(0,1920),0,20,0,0)
diffX = window.winfo_pointerx() - newbul.realX
diffY = window.winfo_pointery() - newbul.realY
scale = newbul.speed / (diffX ** 2 + diffY ** 2) ** 0.5
newbul.vx = diffX*scale
newbul.vy = diffY*scale
bullets.append(newbul)
timer = time()
for bullet in bullets:
# Update bullet position
bullet.realX += bullet.vx
bullet.realY += bullet.vy
def draw():
global canvas
canvas.delete('bullet')
for bullet in bullets:
canvas.create_oval(bullet.realX-bullet.r,bullet.realY-bullet.r,bullet.realX+bullet.r,bullet.realY+bullet.r,tag="bullet")
if bullet.realX > 1920 or bullet.realY > 1080:
bullets.remove(bullet)
It is slow specifically because you are deleting and redrawing the bullets. Instead of that, you should be moving the existing bullets rather than redrawing them. The canvas has known performance problems when you create lots of canvas items, even if you delete the canvas items. Canvas item ids are not recycled, so the list of item ids that the canvas must maintain grows without bounds and makes the canvas slower on each iteration.
It's also slow because you are trying to redraw 1000 times per second even though your eye can't perceive anything close to th at. You are wasting a lot of cpu cycles doing something which you cannot perceive.
Here is similar advice from someone who has worked on the underlying Canvas implementation: Tkinter GUI app runs slower as time goes on
Related
I want to display some text close to the handles of crosshair ROI. The text is mirrored and I don't know why or how to fix it.
The following code runs, where the class CrossHair is a slight modification of the CrosshairROI given at https://pyqtgraph.readthedocs.io/en/latest/_modules/pyqtgraph/graphicsItems/ROI.html#ROI. More precisely, all I did was setting lock aspect to be False and making another handle to deal with another direction.
import pyqtgraph as pg
from PyQt5.QtWidgets import*
from PyQt5.QtCore import*
from PyQt5.QtGui import*
class MainWindow(pg.GraphicsLayoutWidget):
def __init__(self):
super().__init__()
layout = self.addLayout()
self.viewbox = layout.addViewBox(lockAspect=True)
self.viewbox.setLimits(minXRange = 200, minYRange = 200,maxXRange = 200,maxYRange = 200)
self.crosshair = CrossHair()
self.crosshair.setPen(pg.mkPen("w", width=5))
self.viewbox.addItem(self.crosshair)
class CrossHair(pg.graphicsItems.ROI.ROI):
def __init__(self, pos=None, size=None, **kargs):
if size is None:
size=[50,50]
if pos is None:
pos = [0,0]
self._shape = None
pg.graphicsItems.ROI.ROI.__init__(self, pos, size, **kargs)
self.sigRegionChanged.connect(self.invalidate)
self.addScaleRotateHandle(pos = pg.Point(1,0), center = pg.Point(0, 0))
self.addScaleRotateHandle(pos = pg.Point(0,1), center = pg.Point(0,0))
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
if self._shape is None:
x_radius, y_radius = self.getState()['size'][0],self.getState()['size'][1]
p = QPainterPath()
p.moveTo(pg.Point(-x_radius, 0))
p.lineTo(pg.Point(x_radius, 0))
p.moveTo(pg.Point(0, -y_radius))
p.lineTo(pg.Point(0, y_radius))
p = self.mapToDevice(p)
stroker = QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = self.mapFromDevice(outline)
return self._shape
def paint(self, p, *args):
x_radius, y_radius = self.getState()['size'][0],self.getState()['size'][1]
p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.setPen(self.currentPen)
p.drawLine(pg.Point(0, -y_radius), pg.Point(0, y_radius))
p.drawLine(pg.Point(-x_radius, 0), pg.Point(x_radius, 0))
x_pos, y_pos = self.handles[0]['item'].pos(), self.handles[1]['item'].pos()
x_length, y_length = 2*x_radius, 2*y_radius
x_text, y_text = str(round(x_length,2)) + "TEXT",str(round(y_length,2)) + "TEXT"
p.drawText(QRectF(x_pos.x()-50, x_pos.y()-50, 100, 100), Qt.AlignmentFlag.AlignLeft, x_text)
p.drawText(QRectF(y_pos.x()-50, y_pos.y()-50, 100, 100), Qt.AlignmentFlag.AlignBottom, y_text)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()
We see that:
The objective is to fix the above code such that:
It displays texts dependent on the length of the line (2*radius) close to each handle without reflecting.
The text is aligned close to the handle such that no matter how the user rotates the handle the text is readable (i.e. not upside down).
I am having great deal of trouble with the first part. The second part can probably be fixed by changing aligning policies but I don't know which one to choose .
The reason of the inversion is because the coordinate system of pyqtgraph is always vertically inverted: similarly to the standard convention of computer coordinates, the reference point in Qt is always considered at the top left of positive coordinates, with y > 0 going down instead of up.
While, for general computer based imaging this is fine, it clearly doesn't work well for data imaging that is commonly based on standard Cartesian references (positive values of y are always "above"). And that's what pyqtgraph does by default.
The result is that, for obvious reasons, basic drawing that is directly done on an active QPainter will always be vertically inverted ("mirrored"). What you show in the image is the result of a composition of vertical mirroring and rotation, which is exactly the same as horizontal mirroring.
To simplify: when p is vertically mirrored, it becomes b, which, when rotated by 180°, results in q.
There's also another issue: all pyqtgraph items are actually QGraphicsItem subclasses, and one of the most important aspects of QGraphicsItems is that their painting is and shall always be restricted by its boundingRect():
[...] all painting must be restricted to inside an item's bounding rect. QGraphicsView uses this to determine whether the item requires redrawing.
If you try to move the handles very fast, you'll probably see some drawing artifacts ("ghosts") in the text caused by the painting buffer that is used to improve drawing performance, and that's because you didn't consider those elements in the boundingRect() override: the painting engine didn't know that the bounding rect was actually bigger, and didn't consider that the previously drawn regions required repainting in order to "clear up" the previous content.
Now, since those are text displaying objects, I doubt that you're actually interested in having them always aligned to their respective axis (which is not impossible, but much more difficult). You will probably want to always display the values of those handles to the user in an easy, readable way: horizontally.
Considering the above, the preferred solution is to use child items for the text instead of manually drawing it. While, at first sight, it might seem a risk for performance and further complication, it actually ensures 2 aspects:
the text items will always be properly repainted, and without any "ghost residue" caused by the wrong bounding rect;
the performance loss is practically little to none, since item management (including painting) is completely done on the C++ side;
For that, I'd suggest the pg.TextItem class, which will also completely ignore any kind of transformation, ensuring that the text will always be visible no matter of the scale factor.
Note that "mirroring" is actually the result of a transformation matrix that uses negative scaling: a scaling of (0, -1) means that the coordinates are vertically mirrored. If you think about it, it's quite obvious: if you have a positive y value in a cartesian system (shown "above" the horizontal axis) and multiply it by -1, that result is then shown "below".
Given the above, what you need to do is to add the two "labels" as children of the handle items, and just worry about painting the two crosshair lines.
Finally, due to the general performance requirements of pyqtgraph (and QGraphicsView in general), in the following example I took the liberty to make some modifications to the original code in order to improve responsiveness:
class CrossHair(pg.graphicsItems.ROI.ROI):
_shape = None
def __init__(self, pos=None, size=None, **kargs):
if size is None:
size = [50, 50]
if pos is None:
pos = [0, 0]
super().__init__(pos, size, **kargs)
self.sigRegionChanged.connect(self.invalidate)
font = QFont()
font.setPointSize(font.pointSize() * 2)
self.handleLabels = []
for refPoint in (QPoint(1, 0), QPoint(0, 1)):
handle = self.addScaleRotateHandle(pos=refPoint, center=pg.Point())
handle.xChanged.connect(self.updateHandleLabels)
handle.yChanged.connect(self.updateHandleLabels)
handleLabel = pg.TextItem(color=self.currentPen.color())
handleLabel.setParentItem(handle)
handleLabel.setFont(font)
self.handleLabels.append(handleLabel)
self.updateHandleLabels()
def updateHandleLabels(self):
for label, value in zip(self.handleLabels, self.state['size']):
label.setText(format(value * 2, '.2f'))
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
if self._shape is None:
x_radius, y_radius = self.state['size']
p = QPainterPath(QPointF(-x_radius, 0))
p.lineTo(QPointF(x_radius, 0))
p.moveTo(QPointF(0, -y_radius))
p.lineTo(QPointF(0, y_radius))
p = self.mapToDevice(p)
stroker = QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = self.mapFromDevice(outline)
return self._shape
def paint(self, p, *args):
p.setRenderHint(QPainter.Antialiasing)
p.setPen(self.currentPen)
x_radius, y_radius = self.state['size']
p.drawLine(QPointF(0, -y_radius), QPointF(0, y_radius))
p.drawLine(QPointF(-x_radius, 0), QPointF(x_radius, 0))
Notes:
pg.Point is actually a subclass of QPointF; unlike helper functions like mkColor() that can be actually necessary for pg objects and are effective in their simplicity/readability, there is really no point (pun intended) to use those subclasses for basic Qt functions, like you're doing for paintEvent(); just use the basic class;
considering the point above, always try to leave object conversion on the C++ side; QPainterPath's moveTo and lineTo always accept floating point values (they are overloaded functions that internally transform the values to QPointF objects); on the other hand, QPainter functions like drawLine only accept individual numeric values as integers (that's why I used QPointF in paintEvent()), so in that case you cannot directly use the coordinate values; always look for the C++ implementation and the accepted value types;
self.getState()['size'] is already a two-item tuple (width and height), retrieving it twice is unnecessary; also, since getState() actually recalls the internal self.state dict, you can avoid the function call (as I did above) as long as getState() is not overridden by a custom subclass;
I am trying to code a boat that keeps its momentum after letting go of "w". However, i also want it to slow down gradually after letting go of "w". I have gotten a decent boat working but sometimes when I press "w" after going a certain direction, the boat goes flying. Is there any way to add a top speed or make it slow down?
self.thrust = 0.0
self.acceleration = 0.0001
self.dx += math.cos(math.radians(self.heading)) * self.thrust
self.dy += math.sin(math.radians(self.heading)) * self.thrust
def accelerate(self):
self.thrust += self.acceleration
def decelerate(self):
self.thrust = 0
If you want it to slow down gradually when you aren't pressing "w", then you may want it to go at the same speed, or faster. This code will work for it to go at the same speed. This should work in any Python IDE.
Suppose your boat is a turtle.
import turtle
Win = turtle.screen()
Boat = turtle.Turtle()
If you want it to also look like a Boat, you can do:
screen.adshape(Saved Document Direction)
Boat.shape(Saved Document Direction)
Otherwise, you can create your own shapes.
Now, we want it to go at a constant speed, while you press "w":
So:
def Constant_Speed():
Boat.speed(Speed You Want it to Go)
Win.listen()
Win.onkeypress(Constant_Speed, "w")
Otherwise, it should slow down.
Initial_Speed = Initial Speed
Boat.speed(Initial_Speed)
Initial_Speed *= Fraction of what you want it to be in/decimal
So, this should work if you replace with your variables:
import turtle
Win = turtle.screen()
Boat = turtle.Turtle()
screen.adshape(Saved Document Direction)
Boat.shape(Saved Document Direction)
def Constant_Speed():
Boat.speed(Speed You Want it to Go)
Win.listen()
Win.onkeypress(Constant_Speed, "w")
Initial_Speed = Initial Speed
Boat.speed(Initial_Speed)
Initial_Speed *= Fraction of what you want it to be in/decimal and must be less than 1
But what if you want it to go faster, not at a constant speed?
Then go to the def Constant_Speed() part.
You can rename it: def Go_Faster()
Now, the code:
Speed = Initial_Speed
Boat.speed(Speed)
Speed *= Times you want to increase speed by
Also:
Change the last line to:
Initial_Speed2 = Initial Speed * Fraction of what you want it to be in/decimal and must be less than 1
With your variables.
Now, this should work:
import turtle
Win = turtle.screen()
Boat = turtle.Turtle()
screen.adshape(Saved Document Direction)
Boat.shape(Saved Document Direction)
def Go_Faster():
Speed = Initial_Speed()
Boat.speed(Speed)
Speed *= Times you want to increase by
Win.listen()
Win.onkeypress(Constant_Speed, "w")
Initial_Speed = Initial Speed
Boat.speed(Initial_Speed)
Initial_Speed2 = Initial Speed * Fraction of what you want it to be in/decimal and must be less than 1
Please provide minimal code example of Sprite following the mouse in Godot. There are complex and big sample projects, but I didn't found nothing small and clear.
Put the Sprite node in the scene, and attach the following script to it.
const SPEED = 500
func _process(delta):
var vec = get_viewport().get_mouse_position() - self.position # getting the vector from self to the mouse
vec = vec.normalized() * delta * SPEED # normalize it and multiply by time and speed
position += vec # move by that vector
I am trying to build an application that requires the user to draw something.
To do so, I create a canvas (a pygame.Surface object) on which the drawing are registered, and then I blit it onto the window. I'd like the canvas to be infinite, so that when the user scrolls he can continue drawing (of course only a small part of the canvas is blited onto the window). But, actually, Surface in pygame requires a finite width and height and, most importantly, it's not that big! (I think that's because it actually locks the space in memory).
So, I tried to create chunks: every chunk has given fixed size (like, twice the screen size), and to each chunk is allocated a certain Surface. Chunks are created dynamically, on demand, and it overall works pretty well.
My problem is that when I try to draw lines that cross onto multiple chunks, it requires a great effort to compute onto which chunks that line should actually be drawn, and in what pieces it should be broken. I didn't even try to draw rectangles because it really was a pain to make the 'draw-a-line' function work.
That's when I thought that what I was doing was fundamentally wrong: instead of trying to rewrite all of pygame.draw and pygame.gfxdraw functions so that they basically do a per-chunk work, I should really overload the pygame.Surface (say, create a MySurface class child of Surface) so whenever a pixel is modified, I internally chose to which chunk it belongs and actually change it on that chunk, and pass that new Surface object to the pygame functions.
I've searched a lot at the pygame doc, but there it isn't explained how to do that. I don't even know what methods of a Surface object are internally called when I blit/draw onto it! I also google it and I didn't find anyone trying to do that kind of stuff (maybe I'm going the wrong way?).
So, my question(s) is: is this the right approach? And, if yes, how should I realize it?
I don't post code because what I need is more an explanation on where to find the doc of what I try to do more than a code review.
You can't just subclass Surface, because it's not written in python, but in C. Here's the source code; look for yourself.
You could take another approach and instead of calculating where to draw stuff, blit it onto a temporary Surface first and blit that to the chunks relative to the chunk's position.
Here's simple example I hacked together:
import pygame
class Chunk(pygame.sprite.Sprite):
def __init__(self, grid_pos, size, color):
super().__init__()
self.image = pygame.Surface(size)
self.rect = self.image.get_rect(
x = grid_pos[0] * size[0],
y = grid_pos[1] * size[1]
)
self.image.fill(pygame.Color(color))
def patch(self, surface):
self.image.blit(surface, (-self.rect.x, -self.rect.y))
def main():
pygame.init()
size = 800, 600
screen = pygame.display.set_mode(size)
chunks = pygame.sprite.Group(
Chunk((0,0), size, 'green'),
Chunk((1,0), size, 'red'),
Chunk((0,1), size, 'blue'),
Chunk((1,1), size, 'yellow')
)
dragging = None
drawing = None
tmp_s = pygame.Surface(size, pygame.SRCALPHA)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 3:
dragging = event.pos
if event.button == 1:
drawing = event.pos
if event.type == pygame.MOUSEBUTTONUP:
if event.button == 3:
dragging = None
if event.button == 1:
drawing = None
for chunk in chunks:
chunk.patch(tmp_s)
if event.type == pygame.MOUSEMOTION:
if dragging:
for chunk in chunks:
chunk.rect.move_ip(event.rel)
screen.fill((0, 0, 0))
chunks.draw(screen)
tmp_s.fill((0,0,0,0))
if drawing:
size = pygame.Vector2(pygame.mouse.get_pos()) - drawing
pygame.draw.rect(tmp_s, pygame.Color('white'), (*drawing, *size), 10)
screen.blit(tmp_s, (0, 0))
chunks.update()
pygame.display.flip()
main()
As you can see, the canvas consists of 4 chunks. Use the right mouse button to move the canvas and the left button to start drawing a rect.
I'm currently working on a maze game which is being written in iPython notbook. I do not have access to pygame and is therefore doing colission detection from scratch.
The code i've got so far is able to move the player around and there's a grid already there for how big the playfield is going to be.
from turtle import *
def line(x1, y1, x2, y2):
pu()
goto(x1,y1)
pd()
goto(x2,y2)
pu()
setup(600,600)
setworldcoordinates(-1,-1,11,11)
class Network:
tracer(30)
ht()
for n in range(0,11):
line(0,n,10,n)
line(n,0,n,10)
tracer(1)
head= heading()
st()
class Figur:
register_shape("figur.gif")
shape("figur.gif")
head = heading()
pu()
setpos(9.5,9.5)
def turtle_up():
if head != 90:
seth(90)
fd(1)
def turtle_down():
if head != 270:
seth(270)
fd(1)
def turtle_left():
if head != 180:
seth(180)
fd(1)
def turtle_right():
if head != 360:
seth(0)
fd(1)
onkey(turtle_up, "Up")
onkey(turtle_down, "Down")
onkey(turtle_left, "Left")
onkey(turtle_right, "Right")
listen()
class Walls:
def tortle():
tracer(30)
t1 = Turtle()
t1.color("green")
t1.left(180)
t1.fd(1)
t1.right(90)
t1.fd(11)
for i in range(1,4):
t1.right(90)
t1.fd(12)
for i in range(1,3):
t1.right(90)
t1.fd(1)
t1.left(90)
for i in range(1,5):
t1.fd(10)
t1.right(90)
Walls.tortle()
tracer(1)
update()
done()
Currently the walls are no near being done. I've just started on them and tried to create a wall around the compete playfield resulting in the hole area being covered in green. The picture of the turtle is one i've created myself but I think it should work without it and just take the regular turtle instead.
SO my main question is: How do I create colission detection for my turtle so it can't go through walls ?
I would think to make a set of Vec2Ds of already visited points, then compare to that every time you move. Here's a simple forward and backwards function:
def no_collision_forward(amount, walls):
movement = 1 if abs(amount) == amount else -1 # If amount is negative, moving down
for i in range(abs(amount)):
cur_pos = pos()
nex_pos = Vec2D(cur_pos[0], cur_pos[1] + movement)
if nex_pos in walls:
return # Can return whatever
else:
forward(movement)
def no_collision_backward(amount, walls):
return no_collision_forward(-amount, walls)
visited_locations should be a set of Vec2Ds or tuples.
This is probably not the most efficient solution, but it works!