efficient way to draw pixel art in pygame - python-3.x

I'm making a very simple pixel art software in pygame. My logic was creating a grid class, which has a 2D list, containing 0's. When I click, the grid approximates the row and column selected, and mark the cell with a number, corresponding to the color. For simplicity, let's say '1'.
The code works correctly, but It's slow. If the number of rows and columns is less or equal than 10, It works perfectly, but if It's more, it's very laggy.
I think the problem is that I'm updating the entire screen everytime, and, since the program has to check EVERY cell, It can't handle a bigger list
import pygame
from grid import Grid
from pincel import Pincel
from debugger import Debugger
from display import Display
from pygame.locals import *
pygame.init()
pygame.mixer_music.load("musica/musica1.wav")
pygame.mixer_music.play(-1)
width = 1300
height = 1300
screen = pygame.display.set_mode((1366, 768), pygame.RESIZABLE)
pygame.display.set_caption("SquareDraw")
#Grid Creator
numberOfRows = 25
numberOfColumns = 25
grid = Grid(numberOfRows, numberOfColumns)
# Medidas
basicX = width / numberOfColumns
basicY = height / numberOfRows
#Tool Creator
pincel = Pincel(2)
#variáveis de controle
running = 1
#Initial values
grid.equipTool(pincel)
#variáveis de controle de desenho
clicking = 0
def drawScreen(screen, grid, rows, columns, basicX, basicY):
for i in range(rows):
for j in range(columns):
if grid[i][j]:
print('yes')
print(i, j)
pygame.draw.rect(screen, (0, 0, 0), (j * basicX, i * basicY, basicX, basicY))
while running:
screen.fill((255, 255, 255))
Display.drawScreen(screen, grid.board, grid.getRows(), grid.getColumns(), basicX, basicY)
pygame.display.flip()
events = pygame.event.get()
for event in events:
if (event.type == pygame.QUIT) or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
running = 0
if event.type == pygame.MOUSEBUTTONDOWN or clicking:
clicking = 1
x, y = pygame.mouse.get_pos()
Debugger.printArray2D(grid.board)
print('')
xInGrid = int(x / basicX)
yInGrid = int(y / basicY)
grid.ferramenta.draw(grid.board, xInGrid, yInGrid)
Debugger.printArray2D(grid.board)
print('')
if event.type == pygame.MOUSEBUTTONUP:
clicking = 0
if event.type == pygame.VIDEORESIZE:
width = event.w
height = event.h
basicX = width / numberOfColumns
basicY = height / numberOfRows
print(width, height)
pygame.quit()
The class grid contains the 2D list. The class "Pincel" marks the cells and The class "Debugger" is just for printing lists or anything related to debugging.
Is there a way to update only the part of the screen that was changed? If so, how can I apply that in my logic?
Thanks in advance :)

A few things:
Use the grid array to store the on\off blocks of the screen. It only gets read when the screen is resized and needs a full redraw
When a new rectangle is turned on, draw the rectangle directly in the event handler and update the grid array. There is no need to redraw the entire screen here.
In the resize event, reset the screen mode to the new size then redraw the entire screen using the grid array. This is the only time you need to do a full redraw.
Here is the updated code:
import pygame
#from grid import Grid
#from pincel import Pincel
#from debugger import Debugger
#from display import Display
from pygame.locals import *
pygame.init()
#pygame.mixer_music.load("musica/musica1.wav")
#pygame.mixer_music.play(-1)
width = 1000
height = 1000
screen = pygame.display.set_mode((width, height), pygame.RESIZABLE)
pygame.display.set_caption("SquareDraw")
#Grid Creator
numberOfRows = 250
numberOfColumns = 250
#grid = Grid(numberOfRows, numberOfColumns)
grid = [[0 for x in range(numberOfRows)] for y in range(numberOfColumns)] # use array for grid: 0=white, 1=black
# Medidas
basicX = width / numberOfColumns
basicY = height / numberOfRows
#Tool Creator
#pincel = Pincel(2)
#xx
running = 1
#Initial values
#grid.equipTool(pincel)
#xx
clicking = 0
def drawScreen(screen, grid, basicX, basicY): # draw rectangles from grid array
for i in range(numberOfColumns):
for j in range(numberOfRows):
if grid[i][j]:
#print('yes')
#print(i, j)
pygame.draw.rect(screen, (0, 0, 0), (j * basicX, i * basicY, basicX, basicY))
screen.fill((255, 255, 255)) # start screen
while running:
events = pygame.event.get()
for event in events:
if (event.type == pygame.QUIT) or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
running = 0
if event.type == pygame.MOUSEBUTTONDOWN or clicking: # mouse button down
clicking = 1
x, y = pygame.mouse.get_pos()
#Debugger.printArray2D(grid.board)
#print('')
xInGrid = int(x / basicX)
yInGrid = int(y / basicY)
grid[yInGrid][xInGrid] = 1 # save this point = 1, for screen redraw (if resize)
pygame.draw.rect(screen, (0, 0, 0), (xInGrid * basicX, yInGrid * basicY, basicX, basicY)) # draw rectangle
#grid.ferramenta.draw(grid.board, xInGrid, yInGrid)
#Debugger.printArray2D(grid.board)
#print('')
pygame.display.flip() # update screen
if event.type == pygame.MOUSEBUTTONUP:
clicking = 0
if event.type == pygame.VIDEORESIZE: # screen resized, must adjust grid height, width
width = event.w
height = event.h
basicX = width / numberOfColumns
basicY = height / numberOfRows
#print(width, height)
screen = pygame.display.set_mode((width, height), pygame.RESIZABLE) # reset screen with new height, width
screen.fill((255, 255, 255)) # clear screen
drawScreen(screen, grid, basicX, basicY) # redraw rectangles
pygame.display.flip() # update screen
pygame.quit()

Related

How to deal with the "pixel index out of range error"?

import pygame
window=pygame.display.set_mode((640, 624))
Image = pygame.image.load('eye.jpg')
while True:
window.blit(Image, (0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_x, mouse_y = pygame.mouse.get_pos()
for row in range(200):
for col in range(200):
r,g,b,a = pygame.Surface.get_at(window, [int(mouse_x - 0.5 * 50) + row, int(mouse_y - 0.5 * 25) + col])
pygame.Surface.set_at(window, [int(mouse_x - 0.5 * 50) + row, int(mouse_y - 0.5 * 25) + col], [255 - r, 255 - g, 255 - b, a])
pygame.display.update()
This is what I have so far, it loads the image and with every mouse click, a square around the mouse inverts the colors. The only problem is when the square touches the borders of the window, it crashes and gives me the error pixel index out of range on line 15.
What I expect to happen is simply the inverted colors, I've got no clue why the crash happens when the borders of the window are touched.

Pygame key.get_pressed acted on unintended object?

I am writing a simple class Block which holds four Rect from pygame. It has a key_press function which would move the four Rect when according keys are pressed. It also has a draw function which draws the four Rect on the screen.
The problem comes within the while loop below. Within the while loop, I want to move the block around with keys. Whenever the current block reaches the bottom of the screen, it stops there and a new block gets created. The key press functionality should now ONLY act on the new block.
However, the problem I have with the codes below is that, after the new block is created, the key press acts on BOTH of the blocks (i.e. pressing right would move both to the right).
so I guess my problem could be summed up into one question:
Why didn't the key press function act on the current block only, but both?
Thank you very much in advance!
import pygame
from pygame.rect import Rect
pygame.init()
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
# Set the height and width of the screen
block_size = 20
row, col = 20, 20
color = RED
size = [block_size * col, block_size * row]
screen = pygame.display.set_mode(size)
class Block(Rect):
shape = [] # hold blocks which are part of the shape
def __init__(self):
self.block1 = Rect(0, block_size, block_size, block_size)
self.block2 = Rect(0, block_size * 2, block_size, block_size)
self.block3 = Rect(0, block_size * 3, block_size, block_size)
self.block4 = Rect(0, block_size * 4, block_size, block_size)
self.shape.append(self.block1)
self.shape.append(self.block2)
self.shape.append(self.block3)
self.shape.append(self.block4)
def key_press(self):
key = pygame.key.get_pressed()
if key[pygame.K_LEFT]:
for item in self.shape:
item.move_ip(-block_size, 0)
if key[pygame.K_RIGHT]:
for item in self.shape:
item.move_ip(block_size, 0)
if key[pygame.K_UP]:
for item in self.shape:
item.move_ip(0, -block_size)
if key[pygame.K_DOWN]:
for item in self.shape:
item.move_ip(0, block_size)
def draw(self, surface):
for item in self.shape:
pygame.draw.rect(surface, color, item)
def get_bottom(self):
return self.block4.bottom
done = False
clock = pygame.time.Clock()
curr_block = Block()
while not done:
clock.tick(10)
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done = True # Flag that we are done so we exit this loop
screen.fill(WHITE)
curr_block.key_press()
if curr_block.get_bottom() >= block_size * row:
curr_block = Block()
curr_block.draw(screen) # draw the falling block
pygame.display.flip()
pygame.quit()
Every time when a new block is created by curr_block = Block(), then 4 new elements are append at the end of the list self.shape. If you only want to move the most resent blocks, then then you have to move the last 4 elements of self.shape. The last 4 elements can be accessed by self.shape[-4:] (See Subscriptions). For instance:
class Block(Rect):
# [...]
def key_press(self):
key = pygame.key.get_pressed()
for item in self.shape[-4:]:
if key[pygame.K_LEFT]:
item.move_ip(-block_size, 0)
if key[pygame.K_RIGHT]:
item.move_ip(block_size, 0)
if key[pygame.K_UP]:
item.move_ip(0, -block_size)
if key[pygame.K_DOWN]:
item.move_ip(0, block_size)

moving a square in a grid

making a square move in a grid
import pygame
from pygame.locals import *
pygame.init()
clock = pygame.time.Clock()
w = 1008
h = 640
left = 16
top = 16
width = 16
height = 16
YELLOW = (255, 255, 55)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
bg = (255, 255, 255)
x = 0
y = 0
screen = pygame.display.set_mode((w, h))
gameExit = False
while not gameExit:
for event in pygame.event.get():
if event.type == pygame.QUIT:
gameExit = True
if event.type == KEYDOWN:
if event.key == K_DOWN:
top += 16
if event.key == K_UP:
top -= 16
if event.key == K_RIGHT:
left += 16
if event.key == K_LEFT:
left -= 16
x += 16
y += 16
screen.fill(BLACK)
pygame.draw.rect(screen, [255, 255, 55], [left, top, width, height], 0)
pygame.draw.line(screen, bg, (w - 16, y), (16, y), 1)
pygame.draw.line(screen, bg, (x, h - 16), (y, 16), 1)
pygame.display.flip()
pygame.quit()
at the moment if I # out screen.fill() grid appears on the screen and the rect moves with a tail behind, if I take away the # from the screen.fill() grid disappears but rect moves correct with no tail I want both to happen.
The issue with your code is that it isn't doing what you might expect. Every time you iterate through your while loop, you increment the x and y values by 16 and draw a single horizontal and vertical line. This appears to draw a grid, because the screen is NOT cleared automatically between your while loop iterations. However, as a result, after a while, your x and y values increase to the point that you start drawing more and more grid lines that are off the screen (try to print out the start and end points for these lines in your loop if this is unclear)!
When you add the screen.fill(BLACK) call before you start drawing grid lines, you basically clear the entire screen. Since you do not redraw your old grid lines, only the next pair of grid lines are drawn and the rest are lost. Since the new pair of grid lines eventually goes off the screen (as described in the first paragraph), your code does not appear to draw any grid lines.
When you comment out screen.fill(BLACK) instead, previous drawings are not cleared. All of the grid is drawn over multiple while loop iterations (in addition to wastefully drawing new grid lines off screen), as is all of the previously positions your player is drawn at.
To solve this, your game drawing loop should be structured like this:
Clear the screen.
Draw the entire grid, not just two lines at a time. Do this in a function (e.g. draw_grid).
Draw the rectangle at the new player position. Do this in a function as well (e.g. draw_player).
Rinse and repeat next iteration!
Implementing these small changes, your code could look like this:
import pygame
from pygame.locals import *
def draw_grid():
screen.fill(BLACK)
for y in range(height, h, height):#horizontal lines
pygame.draw.line(screen, bg, (width, y), (w - width, y), 1)
for x in range(width, w, width):#vertical lines
pygame.draw.line(screen, bg, (x, height), (x, h - height), 1)
def draw_player():
pygame.draw.rect(screen, [255, 255, 55], [left, top, width, height], 0)
pygame.init()
clock = pygame.time.Clock()
w = 1008
h = 640
left = 16
top = 16
width = 16
height = 16
YELLOW = (255, 255, 55)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
bg = (255, 255, 255)
x = 0
y = 0
screen = pygame.display.set_mode((w, h))
gameExit = False
while not gameExit:
for event in pygame.event.get():
if event.type == pygame.QUIT:
gameExit = True
if event.type == KEYDOWN:
if event.key == K_DOWN:
top += 16
if event.key == K_UP:
top -= 16
if event.key == K_RIGHT:
left += 16
if event.key == K_LEFT:
left -= 16
draw_grid()
draw_player()
pygame.display.flip()
pygame.quit()
That being said, there is plenty of room for improvement for this code, which I will leave up to you. The big issue is that you are using a bunch of global variables. These are basically all of the variables listed before your while loop. Use of such global variables is "evil" when they are mutable. As your code gets bigger, it will become more difficult to reason what functions are changing these variables, since every function could potentially have access to these variables.

Mouse click detection in pygame

I have a task, where I have a square 3X3 and for every click in the small square, this square will paint in red. Here is my code yet. I think I did something wrong in my first while loop, but I'm not sure. Please help me.
import pygame
pygame.init()
#create a screen
screen = pygame.display.set_mode((400, 400))
#colors
white = [255, 255, 255]
red = [255, 0, 0]
x = 0
y = 0
#create my square
for j in range(3):
for i in range(3):
pygame.draw.rect(screen, white, (x, y, 30, 30), 1)
x += 30
if x == 90:
x = 0
y += 30
pygame.display.flip()
running = 1
while running:
event = pygame.event.poll()
#found in what position my mouse is
if event.type == pygame.QUIT:
running = 0
elif event.type == pygame.MOUSEMOTION:
print("mouse at (%d, %d)" % event.pos)
mouse = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()
#mouse click
if click[0] == 1 and x in range(30) and y in range (30):
pygame.draw.rect(screen, red, (30, 30 , 29 ,29))
while pygame.event.wait().type != pygame.QUIT:
pygame.display.change()
You have to update your screen each time when you draw or do something in screen. So, put this line under your first while loop.
pygame.display.flip()
In your condition you was checking x and y and which are not mouse position.
if click[0] == 1 and x in range(30) and y in range (30):
Check your mouse position in range(90) because of you have three rectangular and the are 30x30.
if click[0] == 1 and mouse[0] in range(90) and mouse[1] in range (90):
Then, set your rectangular start position to fill the mouse point.
rect_x = 30*(mouse[0]//30) # set start x position of rectangular
rect_y = 30*(mouse[1]//30) # set start y position of rectangular
You can edit your code with this.
while running:
pygame.display.flip()
event = pygame.event.poll()
#found in what position my mouse is
if event.type == pygame.QUIT:
running = 0
elif event.type == pygame.MOUSEMOTION:
print("mouse at (%d, %d)" % event.pos)
mouse = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()
#mouse click
if click[0] == 1 and mouse[0] in range(90) and mouse[1] in range (90):
'''
rect_x = 30*(0//30) = 0
rect_y = 30*(70//30) = 60
'''
rect_x = 30*(mouse[0]//30) # set start x position of rectangular
rect_y = 30*(mouse[1]//30) # set start y position of rectangular
pygame.draw.rect(screen, red, (rect_x, rect_y , 30 , 30)) # rectangular (height, width), (30, 30)

Bullet to Enemy Collision Detection

After a few hours of looking for code to reference. I failed i tried to re-write the collision code but it failed. Now my Question is
How can i fix my code to make it delete the Enemy ship when the bullet hits him
Note: Im not asking for you to do it, i just need it further explained what the problem or where im making a mistake
The Code Segment
for bullet in Bullets:
Hit_list = pygame.sprite.Group()
if pygame.sprite.collide_rect(bullet, Enemy_list):
Hit_list.add(hit)
for hit in Hit_list:
Bullets.remove(bullet)
All.remove(bullet)
All.remove(hit)
Enemy_list.remove(hit)
# If the bullet goes off the screen it deletes
if bullet.rect.y < 0:
Bullets.remove(bullet)
All.remove(bullet)
The Error
Traceback (most recent call last):
File "C:\Users\jb127996\Desktop\Invaders in Space\Space Shooter.py", line 92, in <module>
if pygame.sprite.collide_rect(bullet, Enemy_list):
File "C:\Python32\lib\site-packages\pygame\sprite.py", line 1290, in collide_rect
return left.rect.colliderect(right.rect)
AttributeError: 'Group' object has no attribute 'rect'
Entire Code
import pygame
import Player_func
import AI
from pygame.locals import *
#Background
Background = pygame.image.load('Tempback.png')
Background_size = Background.get_size()
Background_rect = Background.get_rect()
# Screens Resoultion
Width = 640
Height = 480
Scr = pygame.display.set_mode((Width,Height))
# Fps Clock Vars( Clock.tick(FPS) )
Clock = pygame.time.Clock()
FPS = 60
# Colors
White = (255, 255, 255)
Black = (0, 0, 0)
Green = (0, 255, 0)
Red = (255, 0, 0)
Blue = (0, 0, 255)
Orange = (255, 140, 0)
Default = White
# Sprite Lists
Bullets = pygame.sprite.Group()
Enemy_list = pygame.sprite.Group()
All = pygame.sprite.Group()
# Shorting the Class files
AI = AI.Enemy()
Ply = Player_func.Player()
# CORDS for Player
AI.rect.x, AI.rect.y = 100, 50
AI_pos = (AI.rect.x, AI.rect.y)
Ply.rect.x, Ply.rect.y = 580, 425
P_pos = (Ply.rect.x, Ply.rect.y)
# True false statements and Vars
running = True
Gun_Overload = 0
Fire = 20
while running:
Enemy_list.add(AI)
All.add(AI)
All.add(Ply)
# Collision Detection for walls on the spaceship
if Ply.rect.x <= 0:
Ply.rect.x += 5.5
elif Ply.rect.x >= 590:
Ply.rect.x -= 5.5
# Events
for event in pygame.event.get():
if event.type == QUIT:
running = False
elif event.type == KEYDOWN:
if event.key == K_SPACE and Gun_Overload == Fire:
bullet = Player_func.Bullet()
bullet.rect.x = (Ply.rect.x +15)
bullet.rect.y = Ply.rect.y
Bullets.add(bullet)
All.add(bullet)
Gun_Overload = 0
#Cool Down
if Gun_Overload < Fire:
Gun_Overload += 1
AI.movement()
Ply.Controls()
# Updates the Sprite lists so they can all be drawn
All.update()
# Checks if a bullet hits the enemy on the list
for bullet in Bullets:
Hit_list = pygame.sprite.Group()
if pygame.sprite.collide_rect(bullet, Enemy_list):
Hit_list.add(hit)
for hit in Hit_list:
Bullets.remove(bullet)
All.remove(bullet)
All.remove(hit)
Enemy_list.remove(hit)
# If the bullet goes off the screen it deletes
if bullet.rect.y < 0:
Bullets.remove(bullet)
All.remove(bullet)
Scr.blit(Background, Background_rect)
All.draw(Scr)
pygame.display.flip()
# FPS Clock Loaded in
Clock.tick(FPS)
pygame.quit()
Your If condition uses collide_rect:
pygame.sprite.collide_rect()
Collision detection between two sprites, using rects.
collide_rect(left, right) -> bool
You pass 2 parameters - a Bullet, and a SpriteGroup. The error is telling you that a SpriteGroup does not have a Rect attribute.
I think you wanted to use:
pygame.sprite.spritecollide()
Find sprites in a group that intersect another sprite.
spritecollide(sprite, group, dokill, collided = None) -> Sprite_list
Apart from this, I would change all the variable names to lowercase. I have also noticed that your if bullet.rect.y < 0: is under the collide test. I think you want to remove a Bullet from the screen even if there is no collision.

Resources