I am currently using PyOpenGL with PyGame to write code for a top-down 2D side scroller.
The code shown below will generate a 2D image in a window.
The image that is added is the background. I would now like to add sprites to the background using OpenGL. In other words, I want to overlay one 2D image on top of another. I cannot seem to find a good way to do this.
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
from screenDictionary import *
### Initialize variables ###
gameOn = True
dispWidth = 1920
dispHeight = 1024
bgImg = 'A1_64.png'
bgImg2 = 'HookedFront64.png'
transTuple = (255, 0, 255)
blackTuple = (100, 100, 100)
screenX = 0
screenY = 0
### Initialize display ###
def createDisplay(dispWidth, dispHeight):
pygame.init()
screen = pygame.display.set_mode((dispWidth, dispHeight), DOUBLEBUF|OPENGL)
pygame.display.set_caption('Hooked')
return screen
def createSurfaces(screen):
background = pygame.Surface(screen.get_size())
background.fill(blackTuple)
sprite = pygame.Surface(screen.get_size())
sprite.set_colorkey(transTuple)
return background
def loadScene(bgImg):
img = pygame.image.load(bgImg)
textureData = pygame.image.tostring(img, "RGB", 1)
width = img.get_width()
height = img.get_height()
bgImgGL = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, bgImgGL)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, textureData)
glEnable(GL_TEXTURE_2D)
def placeScene():
glLoadIdentity()
gluPerspective(90, 1, 0.05, 100)
glTranslatef(0,0,0)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
glBegin(GL_QUADS)
glTexCoord2f(0,0)
glVertex3f(-4,-4,-4)
glTexCoord2f(0,1)
glVertex3f(-4,4,-4)
glTexCoord2f(1,1)
glVertex3f(4,4,-4)
glTexCoord2f(1,0)
glVertex3f(4,-4,-4)
glEnd()
screen = createDisplay(dispWidth, dispHeight)
background = createSurfaces(screen)
mapParams = screenDictionary(screenX, screenY)
bgImg = mapParams[0]
loadScene(bgImg)
loadScene(bgImg2)
loadScene(bgImg)
###Run the game###
while gameOn:
placeScene()
pygame.display.flip()
pygame.time.wait(1)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
gameOn = False
Problem solved.
Changes made: used gluOrtho2D instead of gluPerspective. Used glVertex2f instead of glVertex3f.
Most important change: Removed glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
This was clearing the scene every time it was executed... DUH! Still learning openGL, but will leave this here in case anyone else is dumb like me and runs into a similar problem.
Related
I tried to make a 2D texture rendering function in pygame. But it doesn't work. Here is function:
def drawTexture(screen,texture,rect):
last_texture_x, last_texture_y = rect[2],rect[3]
for texture_count_y in range(0,int(rect[3]/texture.get_height())+1):
for texture_count_x in range(0,int(rect[2]/texture.get_width())+1):
screen.blit(texture.subsurface(0,0,min(last_texture_x,texture.get_width()),min(last_texture_y,texture.get_height())),(rect[0]+texture.get_width()*texture_count_x,rect[1]+texture.get_height()*texture_count_y))
last_texture_x -= texture.get_width()
last_texture_y -= texture.get_height()
This function fills a rect with a texture. It fills width well. But it don't fill height when rect height is bigger than texture height. I think problem is texture_count_y variable. When I replace the variable to a number manually, the function works. But variable returns right value when I print it. My head really confused right now. How can I make the function works well?
EDIT:
Red lines mean rect.
Yellow lines mean texture images function uses to fill rect.
It fills columns well but it fills one row.
The problem is simple. last_texture_x must be initialized before the inner loop:
def drawTexture(screen, texture, rect):
last_texture_y = rect[3]
for texture_count_y in range(0,int(rect[3]/texture.get_height())+1):
last_texture_x = rect[2]
for texture_count_x in range(0,int(rect[2]/texture.get_width())+1):
screen.blit(texture.subsurface(0,0,min(last_texture_x,texture.get_width()),min(last_texture_y,texture.get_height())),(rect[0]+texture.get_width()*texture_count_x,rect[1]+texture.get_height()*texture_count_y))
last_texture_x -= texture.get_width()
last_texture_y -= texture.get_height()
However, I recommend to use the features of the pygame.Rect object for this task:
def drawTexture(screen, texture, rect):
target_rect = pygame.Rect(rect)
for x in range(target_rect.left, target_rect.right, texture.get_width()):
for y in range(target_rect.top, target_rect.bottom, texture.get_height()):
clip_rect = texture.get_rect(topleft = (x, y)).clip(target_rect)
screen.blit(texture.subsurface(0, 0, *clip_rect.size), (x, y))
Minimal example:
import pygame
pygame.init()
window = pygame.display.set_mode((500, 300))
clock = pygame.time.Clock()
def drawTexture(screen, texture, rect):
target_rect = pygame.Rect(rect)
for x in range(target_rect.left, target_rect.right, texture.get_width()):
for y in range(target_rect.top, target_rect.bottom, texture.get_height()):
clip_rect = texture.get_rect(topleft = (x, y)).clip(target_rect)
screen.blit(texture.subsurface(0, 0, *clip_rect.size), (x, y))
target_rect = pygame.Rect(25, 25, 450, 250)
texture = pygame.Surface((200, 200))
texture.fill((255, 255, 0))
pygame.draw.rect(texture, (127, 127, 127), (2, 2, 196, 196))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.fill(0)
pygame.draw.rect(window, (255, 255, 255), target_rect, 5)
drawTexture(window, texture, target_rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()
exit()
I have created a complete snake game using C++ and OpenGL before, and I want to do the same using Python, pygame, and PyOpenGL. The current problem I have is that after I spawn a fruit, it does not appear on the screen. Here's the code for my main function:
def main(): # Main function
# Initialize game components
game = Game(800, 600)
test_fruit = game.spawn_fruit(Point(100, 100))
# Initialize pygame module
pygame.init()
pygame.display.set_mode(game.get_window_size(), DOUBLEBUF | OPENGL)
pygame.display.set_caption("Python Game")
# Define variable to control main loop
running = True
# Main loop
while running:
# event handling, gets all event from the event queue
for event in pygame.event.get():
# only do something if the event is of type QUIT
if event.type == pygame.QUIT:
# change the value to False, to exit the main loop
running = False
# Modify game properties
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
game.draw_shapes()
pygame.display.flip()
pygame.time.wait(5)
It's possible I'm missing a pygame or pyopengl function, but I'm not sure. I've also tried changing pygame.display.flip() to pygame.display.update(), yet it gives me an error ("cannot update an OpenGL display") instead.
Here's the code for the shape I am attempting to display:
class Circle:
def __init__(self, pivot: Point, radius: int, sides: int, fill: bool, color: Color):
self.pivot = pivot
self.radius = radius
self.sides = sides
self.fill = fill
self.color = color
# Draw the shape of the circle
def draw(self):
glColor3f(self.color.r, self.color.g, self.color.b)
if self.fill:
glBegin(GL_POLYGON)
else:
glBegin(GL_LINE_LOOP)
for i in range(100):
cosine = self.radius * cos(i*2*pi/self.sides) + self.pivot.x
sine = self.radius * sin(i*2*pi/self.sides) + self.pivot.y
glVertex2f(cosine, sine)
glEnd()
OpenGL coordinates are in range [-1.0, 1.0] (Normalized Device Space). The Normalized device space is a unique cube from the left, bottom, near (-1, -1, -1) to the right, top, far (1, 1, 1).
If you want to use "window" coordinates, you must specify an Orthographic projection using glOrtho:
glOrtho(0, 800, 600, 0, -1, 1)
Choose the matrix mode with glMatrixMode and load the Identity matrix with glLoadIdentity.
Example:
def main(): # Main function
# Initialize game components
game = Game(800, 600)
test_fruit = game.spawn_fruit(Point(100, 100))
# Initialize pygame module
pygame.init()
pygame.display.set_mode(game.get_window_size(), DOUBLEBUF | OPENGL)
pygame.display.set_caption("Python Game")
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, 800, 600, 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# Define variable to control main loop
running = True
# [...]
I am trying to make a tic-tac-toe game with pygame. An important thing I want is being able to make my images (eg. X and O) slightly translucent for when my user is only hovering over a grid tile. I also use opacity to visually show whose turn it is.
This is what I have tried:
x_tile = pygame.image.load('x_tile').convert()
x_tile.set_alpha(100)
This works fine when I'm blitting x_tile directly onto the display like this:
# This is for simplicity's sake. The actual blit process is all being done in an infinite loop
screen = pygame.display.set_mode((300, 300))
screen.blit(x_file, x_file.get_rect())
But my game is using another image that represents the grid, and that is what I'm blitting onto. So I'm blitting this board onto the display, then blitting the actual X and O tiles on the board.
screen = pygame.display.set_mode((300, 300))
screen.blit(board, board_rect)
board.blit(x_tile, x_tile.get_rect(center=grid[0].center)) # I have a list of Rects that make a grid on the board image. grid[0] is the top left
When I do it that way, x_tile.set_alpha(100) seems to have no effect and I don't know what to do.
Edit: I am using pygame 2.0.1. I'm on Windows 10.
Here is the entire code
import os
import pygame
from pygame.locals import *
# Game constants
WIN_SIZE = WIN_WIDTH, WIN_HEIGHT = 800, 600
BLACK = 0, 0, 0
WHITE = 255, 255, 255
RED = 255, 0, 0
BLUE = 0, 0, 255
# Game functions
class NoneSound:
"""dummy class for when pygame.mixer did not init
and there is no sound available"""
def play(self): pass
def load_sound(file):
"""loads a sound file, prepares it for play"""
if not pygame.mixer:
return NoneSound()
music_to_load = os.path.join('sounds', file)
try:
sound = pygame.mixer.Sound(music_to_load)
except pygame.error as message:
print('Cannot load following sound:', music_to_load)
raise SystemExit(message)
return sound
def load_image(file, colorkey=None, size=None):
"""loads image into game"""
image_to_load = os.path.join('images', file)
try:
image = pygame.image.load(image_to_load).convert()
except pygame.error as message:
print('Cannot load following image:', image_to_load)
raise SystemExit(message)
if colorkey is not None:
if colorkey == -1:
colorkey = image.get_at((0, 0))
image.set_colorkey(colorkey, RLEACCEL)
if size is not None:
image = pygame.transform.scale(image, size)
return image
# Game class
class TTTVisual:
"""Controls game visuals"""
def __init__(self, win: pygame.Surface):
self.win = win
# Load in game images
self.board = load_image('board.png', size=(600, 450), colorkey=WHITE)
self.x_tile = load_image('X_tile.png', size=(100, 100), colorkey=BLACK)
self.o_tile = load_image('O_tile.png', size=(100, 100), colorkey=BLACK)
# Translucent for disabled looking tile
self.x_tile_trans = self.x_tile.copy()
self.o_tile_trans = self.o_tile.copy()
self.x_tile_trans.set_alpha(100)
self.o_tile_trans.set_alpha(100)
# Used to let user know whose turn it is
self.x_turn = pygame.transform.scale(self.x_tile, (50, 50))
self.o_turn = pygame.transform.scale(self.o_tile, (50, 50))
self.x_turn_trans = pygame.transform.scale(self.x_tile_trans, (50, 50))
self.o_turn_trans = pygame.transform.scale(self.o_tile_trans, (50, 50))
self.get_rects()
self.grid = self.setup_grid()
def get_rects(self):
"""Creates coords for some visual game assets"""
self.board_rect = self.board.get_rect(
center=self.win.get_rect().center)
self.x_turn_rect = self.x_turn.get_rect(top=10, left=10)
self.o_turn_rect = self.o_turn.get_rect(top=10, left=WIN_WIDTH-60)
def setup_grid(self):
grid = []
left = 0
top = 150
row = 0
for i in range(9):
if (i != 0) and (i % 3 == 0):
row += 1
left = 0
grid.append(pygame.Rect(left, row*top, 200, 150))
left += 200
return grid
def update_turn_status(self):
"""Updates the X and O tiles on the top left and right to
let user know whose turn it is"""
self.win.blits((
(self.x_turn_trans, self.x_turn_rect),
(self.o_turn, self.o_turn_rect)
))
def update_grid(self):
"""Updates board"""
self.win.blit(self.board, self.board_rect)
# Here is where you could change board to win and see that the tile changes in opacity
self.board.blit(self.x_tile_trans, self.x_tile_trans.get_rect(center=self.grid[0].center))
def update(self):
self.win.fill(WHITE)
self.update_turn_status()
self.update_grid()
pygame.display.flip()
def main():
pygame.init()
win = pygame.display.set_mode(WIN_SIZE)
tttvisual = TTTVisual(win)
tttfunc = TTTFunc(tttvisual)
clock = pygame.time.Clock()
running = True
while running:
clock.tick(60)
for event in pygame.event.get():
if event.type == QUIT:
running = False
tttvisual.update()
pygame.quit()
if __name__ == "__main__":
main()
The issue is caused by the line:
self.board.blit(self.x_tile_trans, self.x_tile_trans.get_rect(center=self.grid[0].center))
You don't blit the image on the display Surface, but on the self.board Surface. When a Surface is blit, it is blended with the target. When you draw on a Surface, it changes permanently. Since you do that over and over again, in every frame, the source Surface appears to by opaque. When you decrease the alpha value (e.g. self.x_tile_trans.set_alpha(5)), a fade in effect will appear.
Never draw on an image Surface. Always draw on the display Surface. Cleat the display at begin of a frame. Draw the entire scene in each frame and update the display once at the end of the frame.
class TTTVisual:
# [...]
def update_grid(self):
"""Updates board"""
self.win.blit(self.board, self.board_rect)
# Here is where you could change board to win and see that the tile changes in opacity
x, y = self.grid[0].center
x += self.board_rect.x
y += self.board_rect.y
self.win.blit(self.x_tile_trans, self.x_tile_trans.get_rect(center=(x, y)))
The typical PyGame application loop has to:
handle the events by either pygame.event.pump() or pygame.event.get().
update the game states and positions of objects dependent on the input events and time (respectively frames)
clear the entire display or draw the background
draw the entire scene (blit all the objects)
update the display by either pygame.display.update() or pygame.display.flip()
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 have a program with a player (who is an image) and a rectangle and I want that when the player has a collision with the rectangle, the size of the image increase.
For now, I have this code :
import pygame
from random import randint
WIDTH, HEIGHT = 800, 800
FPS = 60
pygame.init()
win = pygame.display.set_mode((WIDTH, HEIGHT))
fenetre_rect = pygame.Rect(0, 0, WIDTH, HEIGHT)
pygame.display.set_caption("Hagar.io")
clock = pygame.time.Clock()
bg = pygame.image.load("bg.png").convert()
bg_surface = bg.get_rect(center=(WIDTH / 2, HEIGHT / 2))
bg_x = bg_surface.x
bg_y = bg_surface.y
x_max = WIDTH / 2
y_max = HEIGHT / 2
# player
player = pygame.transform.scale(pygame.image.load("player.png").convert_alpha(), (i, i))
player_rect = player.get_rect(center=(x_max, y_max))
# cell
rect_surface = pygame.Rect(300, 500, 20, 20)
# Game loop
running = True
while running:
dt = clock.tick(FPS) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if player_rect.colliderect(rect_surface):
print("collide")
bg_surface.x = bg_x
bg_surface.y = bg_y
# draw on screen
win.blit(bg, bg_surface)
pygame.draw.rect(win, (255, 0, 0), rect_surface)
win.blit(player, player_rect)
pygame.display.flip()
pygame.quit()
I have try to add in the "colliderect" condition but it does not work :
player_rect.width += 1
player_rect.height += 1
Thanks for your help !
This line
player = pygame.transform.scale(pygame.image.load("player.png").convert_alpha(), (i, i))
is using the variable i but it is not defined in your code. I'm not sure where it is defined, but it is key to what you want. I will try to answer without this information anyway:
Thing is, enlarging the rect won't do anything, because a rect is just coordinates. You have to scale the actual image, and pygame.transform.scale does exactly that.
You can keep the image in a separate variable player_img:
player_img = pygame.image.load("player.png").convert_alpha()
player = pygame.transform.scale(player_img, (i, i))
Then when you want to scale it differently, just call .scale() again:
double_size_player = pygame.transform.scale(player_img, (i*2, i*2))
That still leaves us to the mistery of your undefined i variable, but I think you get the gist of it. Remeber that you have to extract a new rect from the scaled image because it will be bigger.