I am working on a platformer game in which I want to add random stars in the background. I have written some code in the function drawBackDrop() which will make some stars at random positions on the screen.
It works fine, but for drawing the stars I have to update it every frame, so the function is called everytime and it makes random stars differently every time.
I want it to appear only once and then not update, so the stars remain at constant positions.
My drawBackDrop() function:
def drawBackdrop():
if globals.theme == "dark":
a = random.randint(3, 12)
x = random.randint(5, globals.screen_width - 12)
y = random.randint(5, globals.screen_height - 12)
numberOfStars = random.randint(35, 55)
starRect = pygame.Rect(x, y, a, a)
for i in range(numberOfStars):
pygame.draw.rect(globals.screen, "White", starRect)
if globals.theme == "light":
pass #Sun and Clouds
This is the code of scene in which I call the function:
def draw(self, sceneManager, screen):
if globals.theme == "light":
screen.fill(globals.light_blue)
elif globals.theme == "dark":
screen.fill(globals.black)
draw.drawBackdrop()
self.cameraSystem.update(screen)
What I get:
As you can see, the stars are being formed every frame. I want it to form only once and then draw the same stars in screen until I win/lose/leave.
How do I only call the function once? Or is there any other way to draw random stars?
There's two ways you can go here.
Paint the stars to a background layer, and blit that every frame
Generate the list of random stars once, and store them. Repainting the same Rects every frame. (As #matszwecja suggested in a comment)
But first note that there's a minor bug in your code. It's not generating the stars correctly - IFF I understand your intentions correctly. It looks like you wanted to generate a random set of stars, but it was painting the same star over and over.
def drawBackdrop():
if globals.theme == "dark":
numberOfStars = random.randint(35, 55) # <<-- MOVED outside loop
for i in range(numberOfStars): # <<-- MOVED Loop
a = random.randint(3, 12)
x = random.randint(5, globals.screen_width - 12)
y = random.randint(5, globals.screen_height - 12)
starRect = pygame.Rect(x, y, a, a)
pygame.draw.rect(globals.screen, "White", starRect)
Background Surface
def makeBackgroundStars():
""" Generate a transparent background overlay with stars """
background = pygame.Surface( ( globals.screen_width, globals.screen_height ), pygame.SRCALPHA )
numberOfStars = random.randint(35, 55)
for i in range(numberOfStars):
a = random.randint(3, 12)
x = random.randint(5, globals.screen_width - 12)
y = random.randint(5, globals.screen_height - 12)
starRect = pygame.Rect(x, y, a, a)
pygame.draw.rect( background, "White", starRect )
return background
The you just need to make the initial image, and then blit it every frame. Probably you'd include all background graphics into this image too.
star_field = makeBackgroundStars()
# in main loop
# ...
globals.screen.blit( star_field, ( 0, 0 ) )
List of Star Rectangles
star_positions = [] # global to hold rects
def genererateStars():
""" generate a random amount of stars for the background """
global star_positions
numberOfStars = random.randint(35, 55) # <<-- MOVED outside loop
for i in range(numberOfStars): # <<-- MOVED Loop
a = random.randint(3, 12)
x = random.randint(5, globals.screen_width - 12)
y = random.randint(5, globals.screen_height - 12)
star_positions.append( pygame.Rect(x, y, a, a) ) # save it for later
def paintStars( screen ):
""" Draw the stars to the background """
global star_positions
for starRect in star_positions:
pygame.draw.rect( screen, "White", starRect )
Related
As the title says. Using batch drawing I get really good performance, even with 4096 sprites. However, since my sprites need to change their underlying image I run into issues with performance. I'm pretty sure I'm doing something silly here, since I specifically created a grid/sprite sheet to handle this effectively. But, of course, I never really use it in any effective manner. I might as well have had 5 different images.
What I really want is to keep the underlying sprite image constant, but shift the visible part based on the "food" metric. Here's the code:
import sys, pyglet, random, time
# Constants.
WIDTH = 1280
HEIGHT = 960
TARGET_FPS = 60
GROWTH_CHANCE = 0.1
fps = 0
screen = pyglet.window.Window(WIDTH, HEIGHT)
random.seed(time.time())
# Here we load universal assets, images, sounds, etc.
grass_tiles_img = pyglet.image.load('grass_tiles.png')
grass_tiles_grid = pyglet.image.ImageGrid(grass_tiles_img, 1, 5)
# Sprite batches.
grass_batch = pyglet.graphics.Batch()
class GrassTile:
'''Define a grass tile which cows can graze on.'''
def __init__(self, x, y, food):
self.food = food
self.sprite = pyglet.sprite.Sprite(grass_tiles_grid[0], x, y,
batch=grass_batch)
def draw(self):
grid_index = (self.food // 20)
self.sprite.image = grass_tiles_grid[grid_index]
return self.sprite
def grow(self):
if random.random() < GROWTH_CHANCE:
self.food = min(self.food + 1, 99)
#screen.event
def on_close():
sys.exit()
#screen.event
def on_draw():
# Clear the screen.
screen.clear()
# Draw grass.
grass_sprites = []
for grass in grass_tiles:
grass_sprites.append(grass.draw())
grass_batch.draw()
# Draw FPS counter.
label = pyglet.text.Label('FPS: ' + str(fps), 'Times New Roman', 12, 10, 10)
label.draw()
def grow_grass(dt):
for grass in grass_tiles:
grass.grow()
def calculate_fps(dt):
global fps
fps = round(min(pyglet.clock.get_fps(), TARGET_FPS))
grass_tiles = [GrassTile(20 * i, 15 * j, 0) for j in range(64) for i in range(64)]
pyglet.clock.schedule_interval(grow_grass, 1 / TARGET_FPS)
pyglet.clock.schedule_interval(calculate_fps, 1 / TARGET_FPS)
pyglet.app.run()
And here's the image so you can run the code:
https://i.imgur.com/kFe91aA.png
Why not just have the image change during grass.grow()?
You don't need to do anything to the grass in the draw phase except draw the batch. Setting the image of a sprite isn't a draw operation, it just changes texture coordinates.
def grow(self):
if random.random() < GROWTH_CHANCE:
self.food = min(self.food + 1, 99)
grid_index = (self.food // 20)
self.sprite.image = grass_tiles_grid[grid_index]
You also shouldn't be recreating the label every draw frame. Create the label beforehand and just update the text. label.text = f'FPS: {fps}'
I started today to program a game with pygame. In the background is a kind of grid on which you will play in the future. But I noticed that with a while loop to update the screen, the grid is redrawn every time and that's a waste of resources, because nothing changes there anyway. Now I thought about not updating the grid-screen in the background and creating a new screen to play on, which will be updated. But then I encountered a problem: When pygame starts a new screen, the last one closes.
So is it smart to have the game board redrawn every time or is there another method where you can leave an item in the background without updating it? Thank you very much for any help. Code and (wrong) approaches follow.
main.py
import field, game
import ctypes
# Variables
def main():
width, height = ctypes.windll.user32.GetSystemMetrics(0), ctypes.windll.user32.GetSystemMetrics(1)
width_scale, height_scale = 5 / 10, 9 / 10
black = (0, 0, 0)
white = (255, 255, 255)
background_color = (214, 237, 255)
refresh_rate = 60
field_size = [10, 18]
screen = pg.display.set_mode([int(width * width_scale), int(height * height_scale)], pg.NOFRAME)
while True:
for event in pg.event.get():
if event.type == pg.KEYDOWN:
pass
screen.fill(background_color)
box_size = field.draw_boxes(screen.get_width(), screen.get_height(), field_size, screen)
field.draw_next_hand()
pg.display.flip()
game.main(width, height, box_size, field_size)
if __name__ == '__main__':
main()
pg.quit()
field.py
import pygame as pg
def draw_boxes(w, h, size, screen):
global start_x, box_size, g_screen, grey, s
g_screen = screen
s = size
box_size = int(w / 2 / (size[0]+1))
start_x = int(w / 2 - size[0] / 2 * box_size)
grey = (122, 122, 122)
for column in range(0, size[0], 1):
for row in range(0, size[1], 1):
pg.draw.rect(screen, grey, [start_x + column * box_size, box_size + row * box_size, box_size, box_size], width= 1)
return box_size
def draw_next_hand():
global box_size, start_x, g_screen, grey
next_hand_size = 4
next_hand_distance = 1
for column in range(0, next_hand_size, 1):
for row in range(0, next_hand_size, 1):
pg.draw.rect(g_screen, grey, [start_x - 2*box_size*next_hand_distance - column * box_size, box_size + row * box_size, box_size, box_size], width=1)
pg.draw.rect(g_screen, grey, [start_x + box_size*s[0] + box_size * next_hand_distance + column * box_size, box_size + row * box_size, box_size, box_size], width=1)
game.py
import pygame as pg
from main import main
def main(width, height, box_size, f_size):
# Variables
white = (255, 255, 255)
black = (0, 0, 0)
grey = (122, 122, 122)
refresh_rate = 60
g_screen = pg.display.set_mode([int(f_size[0] * box_size), int(f_size[1] * box_size)], pg.NOFRAME)
while True:
g_screen.fill(white)
pg.display.flip()
pg.time.delay(refresh_rate)
Before I added the new screen I had "pg.time.delay(refresh_rate)" instead of "game.main()", which caused the background to be constantly redrawn, so I tried to draw another screen over it, which of course didn't work^^
I've already found some entries on stack overflow, but they didn't fit my problem, because it was suggested to change the screen with for example main = False and game = True, but this wouldn't prevent the board from being redrawn
There's a few ways to improve the performance concerning the background image.
Draw once - You can store the background image in a surface object so it only needs to be generated once. Pygame will retain the screen when hidden or minimized.
Only redraw the updated section - Set a clipping rectangle on the screen so only certain pixels get refreshed when the background is redrawn
Only redraw when needed - The game loop is required, but you can conditionally re-render the background
Draw efficiently - Slow down the game loop using the pygame.time.Clock().tick() method
Here's a short program that illustrates these points. It just shows the current date\time on a background of circles.
import pygame as pg
import time
from datetime import datetime as dt
from random import randint
WIDTH = 480
HEIGHT = 600
pg.init()
screen = pg.display.set_mode((WIDTH, HEIGHT))
def rnd(rg): # save some typing
return randint(0,rg)
font_name = pg.font.match_font('arial')
def draw_text(surf, text, size, x, y): # draw text on screen in rect
font = pg.font.Font(font_name, size)
text_surface = font.render(text, True, (rnd(255),rnd(255),rnd(255)))
text_rect = text_surface.get_rect()
text_rect.midtop = (x, y)
surf.blit(text_surface, text_rect)
def make_bg(): # create background image
surf_bg = pg.Surface((WIDTH, HEIGHT))
surf_bg.fill((0,0,0)) # start with black
for i in range(500): # 500 circles
pg.draw.circle(surf_bg,(rnd(255),rnd(255),rnd(255)), (rnd(WIDTH),rnd(HEIGHT)), 15+rnd(50))
return surf_bg
surf_bg = make_bg() # generate circles once, store surface object
#initial background
screen.blit(surf_bg, screen.get_rect()) # draw background, only needed once in Windows
screen.set_clip((10, HEIGHT/2 - 20, WIDTH-10, HEIGHT/2 + 20)) # set active region on screen
lasttick = pg.time.get_ticks() # milliseconds since init
while True:
pg.time.Clock().tick(5) # run loop 5 times per second
pg.event.get() # required in Windows for OS events
if pg.key.get_pressed()[pg.K_SPACE]: quit() # press space to quit
if (pg.time.get_ticks() - lasttick < 1000): continue # only redraw time each second
lasttick = pg.time.get_ticks()
screen.blit(surf_bg, screen.get_rect()) # background, update clip region only
draw_text(screen, str(dt.now()), 30, WIDTH / 2, HEIGHT / 2 - 10) # draw time
pg.display.flip() # swap screen buffer
I want to be able to create some turtles which display values by subclassing turtle.Turtle.
These turtles should display their value as text centered in their own shape. I also want to be able to position the turtles with accuracy, so setting/determining their width and height relative to a given font size is important.
This is my attempt so far:
I think this answer is relevant: How to know the pixel size of a specific text on turtle graphics in python? but it is quite old, and the bounding box it draws around the text is not in the correct position using python 3.8.
import turtle
FONT_SIZE = 32
class Tile(turtle.Turtle):
def __init__(self):
super().__init__(shape="square")
self.penup()
def show_value(self, val):
self.write(val, font=("Arial", FONT_SIZE, "bold"), align="center")
screen = turtle.Screen()
vals = [5, 7, 8, 2]
for i in range(len(vals)):
tile = Tile()
tile_size = (FONT_SIZE / 20)
tile.shapesize(tile_size)
tile.fillcolor("red" if i % 2 == 0 else "blue")
tile.setx(i * FONT_SIZE)
tile.show_value(vals[i])
turtle.done()
It would be very helpful to have Turtle Objects containing text such
as integer values, which can be used to display a variety of puzzles
and games, and can have their own click handlers attached.
Here's the rub, and the (two) reason(s) that approaches using stamp() as suggested in other answers won't work. First, you can't click on a hidden turtle:
from turtle import *
def doit(x, y):
print("Just do it!")
yertle = Turtle()
# comment out the following line if you want `onlick()` to work
yertle.hideturtle()
yertle.shape('square')
yertle.stamp()
yertle.onclick(doit)
done()
Stamps are not clickable entities. Second, you can't even click on a turtle that's behind ink left by this, or another, turtle:
from turtle import *
def doit(x, y):
print("Just do it!")
yertle = Turtle()
yertle.shape('square')
yertle.fillcolor('white')
yertle.onclick(doit)
myrtle = Turtle()
myrtle.shape('turtle')
myrtle.penup()
myrtle.sety(-16)
# comment out the following line if you want `onlick()` to work
myrtle.write('X', align='center', font=('Courier', 32, 'bold'))
myrtle.goto(100, 100) # move myrtle out of the way of clicking
done()
If you click on the letter 'X', nothing happens unless you manage to hit a portion of the square just beyond the letter. My belief is that although we think of the 'X' as dead ink over our live turtle, at the tkinter level they are both similar, possibly both capable of receiving events, so one obscures the click on the other.
So how can we do this? The approach I'm going to use is make a tile a turtle with an image where the images are generate by writing onto bitmaps:
tileset.py
from turtle import Screen, Turtle, Shape
from PIL import Image, ImageDraw, ImageFont, ImageTk
DEFAULT_FONT_FILE = "/Library/Fonts/Courier New Bold.ttf" # adjust for your system
DEFAULT_POINT_SIZE = 32
DEFAULT_OUTLINE_SIZE = 1
DEFAULT_OUTLINE_COLOR = 'black'
DEFAULT_BACKGROUND_COLOR = 'white'
class Tile(Turtle):
def __init__(self, shape, size):
super().__init__(shape)
self.penup()
self.size = size
def tile_size(self):
return self.size
class TileSet():
def __init__(self, font_file=DEFAULT_FONT_FILE, point_size=DEFAULT_POINT_SIZE, background_color=DEFAULT_BACKGROUND_COLOR, outline_size=DEFAULT_OUTLINE_SIZE, outline_color=DEFAULT_OUTLINE_COLOR):
self.font = ImageFont.truetype(font_file, point_size)
self.image = Image.new("RGB", (point_size, point_size))
self.draw = ImageDraw.Draw(self.image)
self.background_color = background_color
self.outline_size = outline_size
self.outline_color = outline_color
def register_image(self, string):
width, height = self.draw.textsize(string, font=self.font)
image = Image.new("RGB", (width + self.outline_size*2, height + self.outline_size*2), self.background_color)
draw = ImageDraw.Draw(image)
tile_size = (width + self.outline_size, height + self.outline_size)
draw.rectangle([(0, 0), tile_size], outline=self.outline_color)
draw.text((0, 0), string, font=self.font, fill="#000000")
photo_image = ImageTk.PhotoImage(image)
shape = Shape("image", photo_image)
Screen()._shapes[string] = shape # underpinning, not published API
return tile_size
def make_tile(self, string):
tile_size = self.register_image(string)
return Tile(string, tile_size)
Other than its image, the only differences a Tile instance has from a Turtle instance is an extra method tile_size() to return its width and height as generic turtles can't do this in the case of images. And a tile's pen is up at the start, instead of down.
I've drawn on a couple of SO questions and answers:
Dump characters (glyphs) from TrueType font (TTF) into bitmaps
How do you set a turtle's shape to a PIL image
And while I'm at it, this answer has been updated to be more system independent:
How to know the pixel size of a specific text on turtle graphics in python?
To demonstrate how my tile sets work, here's the well-know 15 puzzle implemented using them. It creates two tile sets, one with white backgrounds and one with red (pink) backgrounds:
from tileset import TileSet
from turtle import Screen
from functools import partial
from random import shuffle
SIZE = 4
OFFSETS = [(-1, 0), (0, -1), (1, 0), (0, 1)]
def slide(tile, row, col, x, y):
tile.onclick(None) # disable handler inside handler
for dy, dx in OFFSETS:
try:
if row + dy >= 0 <= col + dx and matrix[row + dy][col + dx] == None:
matrix[row][col] = None
row, col = row + dy, col + dx
matrix[row][col] = tile
width, height = tile.tile_size()
x, y = tile.position()
tile.setposition(x + dx * width, y - dy * height)
break
except IndexError:
pass
tile.onclick(partial(slide, tile, row, col))
screen = Screen()
matrix = [[None for _ in range(SIZE)] for _ in range(SIZE)]
white_tiles = TileSet(background_color='white')
red_tiles = TileSet(background_color='pink')
tiles = []
parity = True
for number in range(1, SIZE * SIZE):
string = str(number).rjust(2)
tiles.append(white_tiles.make_tile(string) if parity else red_tiles.make_tile(string))
parity = not parity
if number % SIZE == 0:
parity = not parity
shuffle(tiles)
width, height = tiles[0].tile_size()
offset_width, offset_height = width * 1.5, height * 1.5
for row in range(SIZE):
for col in range(SIZE):
if row == SIZE - 1 == col:
break
tile = tiles.pop(0)
width, height = tile.tile_size()
tile.goto(col * width - offset_width, offset_height - row * height)
tile.onclick(partial(slide, tile, row, col))
matrix[row][col] = tile
screen.mainloop()
If you click on a number tile that's next to the blank space, it will move into the blank space, otherwise nothing happens. This code doesn't guarantee a solvable puzzle -- half won't be solvable due to the random shuffle. It's just a demonstration, the fine details of it, and the tiles themselves, are left to you.
I am trying to make a background of boxes for a simple snake game by iterating through a 2d array and drawing boxes which I've stored as instances of a class BackgroundCube in each part of the array. When I run the program there are no errors, but nothing shows up on the pygame screen.
I've printed the length of each sublist which shows a length of 20, my desired grid size. I've also just printed the entire array which shows what I believe to be instances of the class, something like this: <main.BackgroundCube object at 0x11186e090> would be one entry in the list. So I believe the problem lies in how I'm drawing the rectangles.
python
WIDTH = 400
HEIGHT = 420
screen = pygame.display.set_mode((WIDTH, HEIGHT))
class BackgroundCube:
def __init__(self, x, y, width, height, color):
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
def draw(self, screen):
pygame.draw.rect(screen, self.color, (self.x, self.y, self.width, self.height), 2)
def redrawGameWindow():
for x in range(20):
for y in range(20):
cube2 = background_cube_list[x][y]
cube2.draw(screen)
run = True
background_cube_list = [[0 for x in range(int(WIDTH/20))] for x in range(int((HEIGHT-20)/20))]
while run:
for cube in range(int(WIDTH / 20)):
for cube1 in range(int((HEIGHT - 20) / 20)):
background_cube_list[cube][cube1] = BackgroundCube(cube * 20, cube1 * 20, 20, 20, (144, 144, 144))
clock.tick(30)
redrawGameWindow()
Again, no errors, just a blank white window. Thank you.
You forgot to add
pygame.display.update()
in your main loop. Add it just after redrawGameWindow().
You also need to define clock, which I guess is clock = pygame.time.Clock(). Add it before the main loop.
I am designing a game using graphics.py in python. I initially got everything setup except, the game consists of clicking on a box which would flip the color of the box. eg: if the color of the box clicked was white, it would turn black. My code works for turning the white boxes to black, but wont convert the black boxes to white. I know my if statement in while loop is wrong. I want to know how you could get the value of the color of the rectangle in graphics.py so I could make a proper if statement.
# _______________________IMPORTS_________________________
from graphics import *
import random
#________________________________________________________
win = None
m_board = []
# Description:
# Wait for the user to enter a valid move via the mouse. If the player selects a position
# outside the valid range or selects an occupied board space, the player is asked again.
# The function returns the move only when it's valid.
# Return value:
# An integer in the range 0 to 99 representing the move
def make_move():
pos = win.getMouse()
x_axis = pos.x // 50
y_axis = pos.y // 50
move = y_axis * 10 + x_axis
return move
# Description:
# Creating the initial board with random black and white boxes
# Return Value:
# None
def draw_board():
global win, m_board
color = ["white", "black"] #Creating list for the random black/white
win = GraphWin("LOGICX", 500, 600)
for y in range(0, 500, 50):
for x in range(0, 500, 50):
board_box = Rectangle(Point(x, y), Point(x + 50, y + 50))
#Setting the boxes with random black/white
board_box.setFill(color[random.randint(0, 1)])
#Adding each box to the empty list
m_board.append(board_box)
#Setting outline color to differentiate individual boxes
board_box.setOutline("grey")
board_box.draw(win)
game_running = True
while game_running:
move = make_move()
if m_board[move] == "black":
m_board[move].setFill("white")
else:
m_board[move].setFill("black")
There are two significant problems with this code. The first is this line:
if m_board[move] == "black":
You know that m_board is a list of Rectangle instances, why would you expect it to be equal to "black"? We can get at the fill color this way:
if m_board[move].config["fill"] == "black":
Another approach would have been to wrap the Rectangle instances with your own object class that keeps track of things you need to query/change. The second problem is this combination:
x_axis = pos.x // 50
y_axis = pos.y // 50
move = y_axis * 10 + x_axis
...
m_board[move].setFill("white")
Although double slash (//) does non-remainder division, since pos.x & pos.y are float, the result is a float and using a float list index, i.e m_board[move], causes an error. The simple fix is to have make_move() call int() on what it returns.
My rework of all the code:
import random
from graphics import *
WINDOW_WIDTH, WINDOW_HEIGHT = 500, 500
COLUMNS, ROWS = 10, 10
TILE_WIDTH, TILE_HEIGHT = WINDOW_WIDTH // COLUMNS, WINDOW_HEIGHT // ROWS
COLORS = ["white", "black"] # Creating list for the random black/white
def make_move():
pos = win.getMouse()
x_axis = pos.x // TILE_WIDTH
y_axis = pos.y // TILE_HEIGHT
return int(y_axis * COLUMNS + x_axis)
def draw_board():
board = []
for y in range(0, WINDOW_HEIGHT, TILE_HEIGHT):
for x in range(0, WINDOW_WIDTH, TILE_WIDTH):
board_box = Rectangle(Point(x, y), Point(x + TILE_WIDTH, y + TILE_HEIGHT))
# Set the boxes with random black/white
board_box.setFill(random.choice(COLORS))
# Set outline color to differentiate individual boxes
board_box.setOutline("grey")
# Add each box to the empty list
board.append(board_box)
board_box.draw(win)
return board
win = GraphWin("LOGICX", WINDOW_WIDTH, WINDOW_HEIGHT)
m_board = draw_board()
game_running = True
while game_running:
move = make_move()
if m_board[move].config["fill"] == "black":
m_board[move].setFill("white")
else:
m_board[move].setFill("black")