I'll explain what I'm doing before I get to the exact problem.
I've been making a 2D game in python. Originally I was making it in pygame but I faced a problem. To prevent a very low frame rate I either had to reduce the screen dimensions and remove features or use another library instead. I'm converting my game to use OpenGL instead. Easier said than done it seems.
I have made a module which manages the screen and graphics but also contains a class called Surface which acts like the pygame surfaces. I then have functions for loading images and converting pygame things to my Surface class objects. I have a function for adding a line to the screen and another for a line strip.
Now the problem I would like to alleviate is that during a minigame in the game, which involves lots of lines, the game slows down over time despite the number of lines being rendered staying reasonably stable. To clean the contents of my surface objects I must use the fill method. This is done and that isn't the problem.
Please don't waste your time if you have to think about it too much but I'd very much appreciate any help.
This is the module which does have some parts incomplete or unconverted which you can just ignore.
Edit: It may be a problem with the fonts instead. I'm not sure but there isn't a reason for why the problem should be with the lines.
#!/usr/bin/env python2.3
#
# Automatic Game Scaling and 2D surface library for pygame and OpenGL
#
# Allows resize of a Window while scaling the game, keeping the aspect ratio.
#
# Created by Matthew Mitchell on 13/09/2009.
# Copyright (c) 2009 Matthew Mitchell. All rights reserved.
#
#Import modules
import sys,os
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
MUSICEND = USEREVENT
def get_resolution(screen,ss,gs):
gap = float(gs[0]) / float(gs[1])
sap = float(ss[0]) / float(ss[1])
if gap > sap:
#Game aspect ratio is greater than screen (wider) so scale width
factor = float(gs[0]) /float(ss[0])
new_h = gs[1]/factor #Divides the height by the factor which the width changes so the aspect ratio remians the same.
game_scaled = (ss[0],new_h)
elif gap < sap:
#Game aspect ratio is less than the screens.
factor = float(gs[1]) /float(ss[1])
new_w = gs[0]/factor #Divides the width by the factor which the height changes so the aspect ratio remians the same.
game_scaled = (new_w,ss[1])
else:
game_scaled = screen.get_size()
return game_scaled
def sound(f):
if len(sys.argv) > 1:
if sys.argv[1] == "-m":
return DummySound()
return pygame.mixer.Sound(os.path.dirname(sys.argv[0]) + f)
def play_music(f):
if f != "":
mute = False
if len(sys.argv) > 1:
if sys.argv[1] == "-m":
mute = True
if not mute:
global music_stop
music_stop = False
pygame.mixer.music.load(os.path.dirname(sys.argv[0]) + f)
pygame.mixer.music.play(0)
pygame.mixer.music.set_endevent(MUSICEND)
def draw_texture( (x,y), (w,h), texture,colour,data,size):
#(x,y) is position, (w,h) is scaled size
glMatrixMode(GL_MODELVIEW)
glLoadIdentity() #Loads model matrix
glBindTexture(GL_TEXTURE_2D, texture) #Binds the current 2D texture to the texture to be drawn
if data != None:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) #Required to be set for maping the pixel data
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) #Similar as above
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size[0], size[1], 0, GL_RGBA,GL_UNSIGNED_BYTE, data) #Put surface pixel data into texture
glColor4fv((1,1,1,1))
else:
glColor4fv(colour)
glBegin(GL_QUADS) #Begin the Quadrilateral
glTexCoord2f(0.0, 0.0) ; glVertex2f(x, y) #Top Left
glTexCoord2f(0.0, 1.0) ; glVertex2f(x, y+h) #Bottom Left
glTexCoord2f(1.0, 1.0) ; glVertex2f(x+w, y+h) #Bottom, Right
glTexCoord2f(1.0, 0.0) ; glVertex2f(x+w, y) #Top, Right
glEnd() #End the Quadrilateral
def open_image(path):
img = pygame.image.load(path)
surf = Surface(img.get_size())
surf.data = pygame.image.tostring(img, "RGBA")
return surf
def draw_line(surface,a,b,c,w):
glMatrixMode(GL_MODELVIEW)
glLoadIdentity() #Loads model matrix
offset = surface.get_offset()
glDisable(GL_LINE_SMOOTH)
glDisable(GL_TEXTURE_2D) #Diable textures temporarily.
glColor4fv(c) #Set Colour
glLineWidth(w) #Set width
glBegin(GL_LINES) #Begin the line
glVertex2i(a[0] + offset[0],a[1] + offset[1]) #Point A
glVertex2i(b[0] + offset[0],b[1] + offset[1]) #Point B
glEnd()
glEnable(GL_TEXTURE_2D)
glEnable(GL_LINE_SMOOTH)
def draw_lines(surface,coordinates,c,w):
glMatrixMode(GL_MODELVIEW)
glLoadIdentity() #Loads model matrix
offset = surface.get_offset()
glDisable(GL_TEXTURE_2D) #Diable textures temporarily.
glColor4fv(c) #Set Colour
glLineWidth(w) #Set width
glBegin(GL_LINE_STRIP) #Begin the line
for c in coordinates: #Loop though the coordinates and add them as vertices.
glVertex2i(c[0] + offset[0],c[1] + offset[1])
glEnd()
glEnable(GL_TEXTURE_2D)
def add_line(surface,c,a,b,w = 1):
c = [float(sc)/255 for sc in c] #Divide colours by 255 because OpenGL uses 0-1
if len(c) != 4:
c.append(1) #Add a value for aplha transparency if needed
surface.children.append([1,[surface,a,b,c,w]]) #1 states it is a line
def add_lines(surface,c,coordinates,w =1):
c = [float(sc)/255 for sc in c] #Divide colours by 255 because OpenGL uses 0-1
if len(c) != 4:
c.append(1) #Add a value for aplha transparency if needed
surface.children.append([1,[surface,coordinates,c,w]]) #1 states it is a line
def font_convert(font):
surf = Surface(font.get_size())
surf.data = pygame.image.tostring(font, "RGBA")
return surf
class DummySound():
def play(self):
pass
def set_volume(self):
pass
class Surface():
def __init__(self,size,extra = None):
self.__offset = (0,0)
self.children = []
self.blitted = False
self.last_offset = (0,0)
self.size = size
self.colour = (0,0,0,0)
self.data = None
def blit(self,surface,offset,area=None, special_flags = 0):
try:
offset_before_last = surface.last_offset
surface.last_offset = (offset[0] + self.__offset[0],offset[1] + self.__offset[1])
self.children.append([0,surface]) #0 states it is a surface
if surface.get_offset() != surface.last_offset or not surface.blitted:
surface.__set_offset(surface.last_offset)
self.__recursive_offset_add(surface,offset_before_last,surface.last_offset) #Add to the children's offsets
surface.blitted = True
except AttributeError:
pass
def __recursive_offset_add(self,surface,offset_before_last,last_offset):
for child in surface.children:
try:
child.__set_offset((child.get_offset()[0] - offset_before_last[0] + last_offset[0],child.get_offset()[1] - offset_before_last[1] + last_offset[1]))
self.__recursive_offset_add(child,offset_before_last,last_offset)
except AttributeError:
pass
def get_offset(self):
return self.__offset
def __set_offset(self,offset):
self.__offset = offset
def fill(self,colour):
colour = [float(b)/255 for b in colour]
if len(colour) < 4:
colour.append(1)
self.children = []
self.colour = colour
def get_size(self):
return self.size
def get_width(self):
return self.size[0]
def get_height(self):
return self.size[1]
class ScaledScreen(Surface):
game_size = None
first_screen = None
screen = None
fs = False #Fullscreen false to start
clock = None
resize = True
game_gap = None
game_scaled = None
title = None
fps = -1
enter_fullscreen = False
exit_fullscreen = False
scale_to_screen = False
iconify = False
on_focus_fullscreen = False
f_key = False
def __init__(self,title,game_size,on_exit):
pygame.init()
self.title = title
self.game_size = game_size
screen_info = pygame.display.Info() #Required to set a good resolution for the game screen
self.first_screen = (screen_info.current_w, screen_info.current_h - 120) #Take 120 pixels from the height because the menu bar, window bar and dock takes space
pygame.display.set_caption(self.title)
self.clock = pygame.time.Clock()
self.game_gap = (0,0)
self.on_exit = on_exit
self.mod_key = 1024 if sys.platform == "darwin" else 64
pygame.display.set_mode(self.first_screen,RESIZABLE|DOUBLEBUF|OPENGL)
#OpenGL Parts
Surface.__init__(self,game_size)
self.textures = []
def update(self,events):
#Updates screen properly
win_size_done = False #Changes to True if the window size is got by the VIDEORESIZE event below
for event in events:
if event.type == QUIT:
self.on_exit()
if event.type == VIDEORESIZE:
ss = [event.w,event.h]
self.resize = True
win_size_done = True
keys = pygame.key.get_pressed() #Get the pressed keys
if pygame.key.get_mods() & self.mod_key:
if(keys[K_q] or keys[K_w]):
self.on_exit()
if keys[K_f]:
if self.f_key == False:
self.f_key = True
if self.fs == False:
self.enter_fullscreen = True
else:
self.exit_fullscreen = True
else:
self.f_key = False
if self.on_focus_fullscreen and pygame.display.get_active():
self.on_focus_fullscreen = False
self.enter_fullscreen = True
if self.enter_fullscreen:
pygame.mouse.set_visible(False)
self.screen = pygame.display.set_mode((self.first_screen[0],self.first_screen[1]+ 120))
if self.scale_to_screen:
self.game_scaled = (self.screen.get_width(),self.screen.get_height())
else:
self.game_scaled = get_resolution(self.screen,(self.screen.get_width(),self.screen.get_height()),self.game_size)
self.game_gap = [(self.screen.get_width() - self.game_scaled[0])/2,(self.screen.get_height() - self.game_scaled[1])/2]
pygame.display.set_mode((0,0), FULLSCREEN|HWSURFACE|DOUBLEBUF|OPENGL)
self.fs = True
self.enter_fullscreen = False
self.resize = False
elif self.exit_fullscreen:
pygame.mouse.set_visible(True)
pygame.display.set_mode(self.first_screen,RESIZABLE|DOUBLEBUF|OPENGL)
self.fs = False
self.resize = True
self.game_gap = (0,0)
self.exit_fullscreen = False
if self.iconify:
self.on_focus_fullscreen = True
#Scale game to screen resolution, keeping aspect ratio
if self.resize:
if(win_size_done == False): #Sizes not gotten by resize event
ss = [self.screen.get_width(),self.screen.get_height()]
self.game_scaled = get_resolution(self.screen,ss,self.game_size)
pygame.display.set_mode(self.game_scaled,RESIZABLE|DOUBLEBUF|OPENGL)
self.resize = False #Next time do not scale unless resize or fullscreen events occur
if self.iconify:
pygame.display.iconify() #Minimise
self.iconify = False
#Open GL Screen setup
glViewport(0,0,self.game_scaled[0],self.game_scaled[1]) #Creates the viewport which is mapped to the window
glEnable(GL_LINE_SMOOTH,GL_FASTEST) #Create antialiased lines
glEnable(GL_BLEND) #Enable alpha blending
glEnable(GL_TEXTURE_2D) #Enable 2D Textures
glDisable(GL_DEPTH_TEST) #Disable depth
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) #The recomended blending functions.
glMatrixMode(GL_PROJECTION)
glLoadIdentity() #Load the projection matrix
gluOrtho2D(0,1280,720,0) #Set an orthorgraphic view
self.draw_game([0,self],(0,0))
pygame.display.flip() #Flip buffer
glDeleteTextures(self.textures) #Remove the redundant texture data from memory.
self.children = []
self.clock.tick(60)
self.fps = self.clock.get_fps()
pygame.display.set_caption(self.title + " - " + str(int(self.fps)) + "fps")
glClear(GL_COLOR_BUFFER_BIT)
def draw_game(self,surface,parent_offset):
if surface[0] == 0:
offset = surface[1].get_offset()
size = surface[1].get_size()
if offset[0] < parent_offset[0]:
size[0] -= parent_offset[0] - offset[0]
offset[0] = parent_offset[0]
if offset[1] < parent_offset[1]:
size[1] -= parent_offset[1] - offset[1]
offset[1] = parent_offset[1]
texture = glGenTextures(1)
self.textures.append(texture)
draw_texture(offset,size,texture,surface[1].colour,surface[1].data,surface[1].get_size()) #Add game to screen with the scaled size and gap required.
for child in surface[1].children:
self.draw_game(child,offset)
else:
if len(surface[1]) == 5:
draw_line(*surface[1])
else:
draw_lines(*surface[1])
class Game(ScaledScreen):
fade = 0
p_key = False
music_stop = False
unfade = False
event_after_fade = -1
loaded = False
fade = 255
unfade = True
homedir = os.path.expanduser("~")
fade_screen = False
keys = []
pygame_events = []
sections = []
back_key = False
transfer_args = ()
def __init__(self,title,game_size,on_exit = sys.exit):
ScaledScreen.__init__(self,title,game_size,on_exit)
self.fade_surface = pygame.Surface([1280,720])
self.fade_surface.fill((0,0,0))
self.fade_surface.set_alpha(0)
def add_section(self,section_object):
self.sections.append(section_object)
def game_loop(self):
self.keys = pygame.key.get_pressed()
self.pygame_events = pygame.event.get()
self.section.loop()
if self.unfade:
if self.fade == 255:
play_music(self.section.music)
if self.fade > 0:
self.fade -= 5
else:
self.music_stop = False
self.unfade = False
if self.fade_screen and not self.unfade: #Fade out
if self.fade == 0:
sound("/sounds/menu3/fade.ogg").play()
self.music_stop = True
pygame.mixer.music.fadeout(850)
if self.fade < 255:
self.fade += 5
else:
self.fade_screen = False
self.unfade = True
if self.fade_screen == False:
if self.event_after_fade != -1:
self.section = self.sections[self.event_after_fade]
self.section.transfer(*self.transfer_args)
self.event_after_fade = -1
self.fade_surface.set_alpha(self.fade)
self.blit(self.fade_surface,(0,0))
for event in self.pygame_events:
if event.type == MUSICEND and self.music_stop == False:
play_music(self.section.music)
self.update(self.pygame_events) #Updates game
def transfer_section(self,section,args=()):
self.transfer_args = args
self.event_after_fade = section
self.fade_screen = True