Any way to refactor this in a neater way? Specifically lines 7-11 in relation to line 39? Using Pygame - python-3.x

I am teaching this to 5th graders so it needs to be as simple as possible. However, as you see inline 39 I am using stored variables for the function parameters. However is there an easier way? Let's say I wanted to put in a 2nd character I would have to put that long list of variables again?
Should I create a character class? If so, how would I plug that into a parameter?
import pygame
pygame.init()
win = pygame.display.set_mode((1000, 1000))
#Character Details
x = 100
y = 5
width = 10
height = 10
vel = 20
run = True
while run:
pygame.time.delay(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
#Movement Details
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
x -= vel #
if keys[pygame.K_RIGHT]:
x += vel
if keys[pygame.K_UP]:
y -= vel
if keys[pygame.K_DOWN]:
y += vel
#Without this character movement is additive.
win.fill((0, 0, 0))
pygame.draw.rect(win, (245, 100, 50), (x, y, width, height))
pygame.display.update()
```

You could make the variables a Rect object.
player1 = pygame.Rect(100,5,10,10) #x,y,w,h
player2 = pygame.Rect(200,20,10,10)
...
pygame.draw.rect(win,(245,100,50),player1)
pygame.draw.rect(win,(245,100,50),player2)
you could also use a list and it would work just as fine, but with the Rect, it calculates extra attributes for you like
player1.center #get center of the player, both x and y
player.right #get right side of player, same as x position + width
you can also move using these
player1.center = (200,200) #moves the center to (200,200)
But you would need to have a velocity variable for each player, which you could have a list

#The Big Kahuna has already suggested that you can replace one of the parameters to the pygame.draw.rect() with a rect representing the player. That is specifically answering your question and you should do that.
Since you are teaching this to others I wanted to suggest some other things as well.
The first is a tweak to what #The Big Kahuna said. He correctly suggested that you should use the the pygame rect class, which you should. Instead of passing the four parameters like that, Rect allows you you create rect's by calling it with the position and size parameters grouped together. I.e you can group the two position parameters and the two size parameters. Like this:
size = (10, 10) # width, height
player1 = pygame.Rect((100, 5), size)
player2 = pygame.Rect((200, 20), size)
You could separate the position out as well if desired.
The other thing that you can do (and the real reason that I am actually commenting here) to make it cleaner is to look at the other parameter to pygame.draw.rect(). The (245,100,50) is the color you are drawing (see the docs here). Instead of just using numbers like that, it can be made more clear by assigning that color to a variable somewhere near the top of your program and using the variable in the draw call. Like this:
orangey = (245, 100, 50)
...
pygame.draw.rect(win, orangey, player1)
In this case I called the color orangey, because when I displayed it it looked kind of orangey to me. but obviously you can name it whatever is appropriately descriptive.
You can use multiple colors to distinguish the player rect's like this:
orange = pygame.Color("orange")
purple = pygame.Color("purple")
...
pygame.draw.rect(win, orange, player1)
pygame.draw.rect(win, purple, player2)
You can see a good way to find defined colors like the ones I showed by looking at the answer to the question here.

Related

Pyqtgraph ROI Mirrors the Displayed Text

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;

Overloading pygame.Surface in order to dynamically augment the size of the surface

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.

Pygame for loop iterates once, and then does not iterate again

Essentially, what I'm doing is taking the enemies list (on line 1), which is holding a list of coordinates and iterate through each pair in the enemies list at the bottom.
I want to go through each enemy in the list, get the y coordinate, add 10 and then go to the next enemy and add 10, so on and so forth. For some reason, it adds 10 ONCE and then stops, and the enemies do not fall down the screen. I don't know why this is happening. Why is it not running through the for loop anymore? Thank you so much for any help.
NOTE: I removed some code at the top for the sake of being less confusing. The update() function is just the pygame flip function.
enemies = [[100,0], [150,0]]
while True:
for enemy in enemies:
x = enemy[0]
y = enemy[1]
y += 10
pygame.draw.rect(screen, (255,0,0), (x, y,10,10))
# uses flip to update the screen
update()
# FPS
clock.tick(20)
You're trying to modify a local variable, not the value in the list. You need to write:
enemy[1] += 10
Since integers are immutable (they cannot be changed), the line y = enemy[1] can be thought of as "copy the value from enemy[1] into y".

Python 3: Find Index of Square in grid, based on Mouse Coordinates

This may have been asked before, but for my lack of correct English terms end up leaving me here. (I'm Finnish) This may be asked before, but what else could I have done?
But I have pygame code, which renders partion of bigger 'map'. I want to have behaviour to 'click' a squre and 'select' it.
The broblem is, how do I find the index of image I am currently overlapping with mouse?
Codelike close to what I have now
#...setup code...
map = [[0,0,0,0], [0,1,0,0], [0,0,0,0]]
while:
render()
#render completely fills the screen with images based on map's objects
mousepos=pyagem.mouse.get_pos()
selectedMapSquare=???
You just have to divide the absolute (screen) coordinates with the size of your squares. So, if the size of your squares is e.g. 32, you can use something like
x, y = pygame.mouse.get_pos()
# TODO: use a constant
w_x, w_y = x / 32, y /32
Now w_x is the index of the x axis, and w_y is the index of the y axis:
# TODO: bound/error checking
tile_under_mouse = map[w_y][w_x]

Get pixel colors of tkinter canvas

I'd like to be able to create and interact with a Tkinter Canvas and, at any time, be able to iterate over each of its pixels and get their RGB values.
Setting pixel by pixel is not necessary, just getting. However, methods analogous to Canvas's create_polygon(), create_line(), create_text(), and create_oval() must be available as well for interacting with the image overall.
There are a number of restraints:
Must work with Python 3
Must work with Linux, Mac, and Windows
Must work with libraries that come with Python (no downloads)
The second restraint is mainly the reason I've posted this question when getting the color of pixels on the screen in Python3.x and several other similar questions already exist.
If this is impossible, what is the closest I can get?
Try it. But is slow :/
from util.color import Color
class ImageUtils:
#staticmethod
def get_pixels_of(canvas):
width = int(canvas["width"])
height = int(canvas["height"])
colors = []
for x in range(width):
column = []
for y in range(height):
column.append(ImageUtils.get_pixel_color(canvas, x, y))
colors.append(column)
return colors
#staticmethod
def get_pixel_color(canvas, x, y):
ids = canvas.find_overlapping(x, y, x, y)
if len(ids) > 0:
index = ids[-1]
color = canvas.itemcget(index, "fill")
color = color.upper()
if color != '':
return Color[color.upper()]
return "WHITE"
It's not possible. The canvas doesn't work that way.
If you're not interested in setting, you can use an image rather than a canvas. You can get the value of individual pixels in a PhotoImage.

Resources