I am working on a little card-swapping world-travel game that I sort of envision as a cross between Bejeweled and the 10 Days geography board games. So far the coding has been going okay, but the frame rate is pretty bad... currently I'm getting low 20's on my Core 2 Duo. This is a problem since I'm creating the game for Intel's March developer competition, which is squarely aimed at netbooks packing underpowered Atom processors.
Here's a screen from the game:
![www.necessarygames.com/my_games/betraveled/betraveled-fps.png][1]
I am very new to Python and Pygame (this is the first thing I've used them for), and am sadly lacking in formal CS training... which is to say that I think there are probably A LOT of bad practices going on in my code, and A LOT that could be optimized. If some of you older Python hands wouldn't mind taking a look at my code and seeing if you can't find any obvious areas for optimization, I would be extremely grateful.
You can download the full source code here:
http://www.necessarygames.com/my_games/betraveled/betraveled_src0328.zip
Compiled exe here:
www.necessarygames.com/my_games/betraveled/betraveled_src0328.zip
One thing I am concerned about is my event manager, which I feel may have some performance wholes in it, and another thing is my rendering... I'm pretty much just blitting everything to the screen all the time (see the render routines in my game_components.py below); I recently found out that you should only update the areas of the screen that have changed, but I'm still foggy on how that accomplished exactly... could this be a huge performance issue?
Any thoughts are much appreciated! As usual, I'm happy to "tip" you for your time and energy via PayPal.
Jordan
Here are some bits of the source:
Main.py
#Remote imports
import pygame
from pygame.locals import *
#Local imports
import config
import rooms
from event_manager import *
from events import *
class RoomController(object):
"""Controls which room is currently active (eg Title Screen)"""
def __init__(self, screen, ev_manager):
self.room = None
self.screen = screen
self.ev_manager = ev_manager
self.ev_manager.register_listener(self)
self.room = self.set_room(config.room)
def set_room(self, room_const):
#Unregister old room from ev_manager
if self.room:
self.room.ev_manager.unregister_listener(self.room)
self.room = None
#Set new room based on const
if room_const == config.TITLE_SCREEN:
return rooms.TitleScreen(self.screen, self.ev_manager)
elif room_const == config.GAME_MODE_ROOM:
return rooms.GameModeRoom(self.screen, self.ev_manager)
elif room_const == config.GAME_ROOM:
return rooms.GameRoom(self.screen, self.ev_manager)
elif room_const == config.HIGH_SCORES_ROOM:
return rooms.HighScoresRoom(self.screen, self.ev_manager)
def notify(self, event):
if isinstance(event, ChangeRoomRequest):
if event.game_mode:
config.game_mode = event.game_mode
self.room = self.set_room(event.new_room)
def render(self, surface):
self.room.render(surface)
#Run game
def main():
pygame.init()
screen = pygame.display.set_mode(config.screen_size)
ev_manager = EventManager()
spinner = CPUSpinnerController(ev_manager)
room_controller = RoomController(screen, ev_manager)
pygame_event_controller = PyGameEventController(ev_manager)
spinner.run()
# this runs the main function if this script is called to run.
# If it is imported as a module, we don't run the main function.
if __name__ == "__main__":
main()
event_manager.py
#Remote imports
import pygame
from pygame.locals import *
#Local imports
import config
from events import *
def debug( msg ):
print "Debug Message: " + str(msg)
class EventManager:
#This object is responsible for coordinating most communication
#between the Model, View, and Controller.
def __init__(self):
from weakref import WeakKeyDictionary
self.listeners = WeakKeyDictionary()
self.eventQueue= []
self.gui_app = None
#----------------------------------------------------------------------
def register_listener(self, listener):
self.listeners[listener] = 1
#----------------------------------------------------------------------
def unregister_listener(self, listener):
if listener in self.listeners:
del self.listeners[listener]
#----------------------------------------------------------------------
def post(self, event):
if isinstance(event, MouseButtonLeftEvent):
debug(event.name)
#NOTE: copying the list like this before iterating over it, EVERY tick, is highly inefficient,
#but currently has to be done because of how new listeners are added to the queue while it is running
#(eg when popping cards from a deck). Should be changed. See: http://dr0id.homepage.bluewin.ch/pygame_tutorial08.html
#and search for "Watch the iteration"
for listener in list(self.listeners):
#NOTE: If the weakref has died, it will be
#automatically removed, so we don't have
#to worry about it.
listener.notify(event)
#------------------------------------------------------------------------------
class PyGameEventController:
"""..."""
def __init__(self, ev_manager):
self.ev_manager = ev_manager
self.ev_manager.register_listener(self)
self.input_freeze = False
#----------------------------------------------------------------------
def notify(self, incoming_event):
if isinstance(incoming_event, UserInputFreeze):
self.input_freeze = True
elif isinstance(incoming_event, UserInputUnFreeze):
self.input_freeze = False
elif isinstance(incoming_event, TickEvent):
#Share some time with other processes, so we don't hog the cpu
pygame.time.wait(5)
#Handle Pygame Events
for event in pygame.event.get():
#If this event manager has an associated PGU GUI app, notify it of the event
if self.ev_manager.gui_app:
self.ev_manager.gui_app.event(event)
#Standard event handling for everything else
ev = None
if event.type == QUIT:
ev = QuitEvent()
elif event.type == pygame.MOUSEBUTTONDOWN and not self.input_freeze:
if event.button == 1: #Button 1
pos = pygame.mouse.get_pos()
ev = MouseButtonLeftEvent(pos)
elif event.type == pygame.MOUSEMOTION:
pos = pygame.mouse.get_pos()
ev = MouseMoveEvent(pos)
#Post event to event manager
if ev:
self.ev_manager.post(ev)
#------------------------------------------------------------------------------
class CPUSpinnerController:
def __init__(self, ev_manager):
self.ev_manager = ev_manager
self.ev_manager.register_listener(self)
self.clock = pygame.time.Clock()
self.cumu_time = 0
self.keep_going = True
#----------------------------------------------------------------------
def run(self):
if not self.keep_going:
raise Exception('dead spinner')
while self.keep_going:
time_passed = self.clock.tick()
fps = self.clock.get_fps()
self.cumu_time += time_passed
self.ev_manager.post(TickEvent(time_passed, fps))
if self.cumu_time >= 1000:
self.cumu_time = 0
self.ev_manager.post(SecondEvent())
pygame.quit()
#----------------------------------------------------------------------
def notify(self, event):
if isinstance(event, QuitEvent):
#this will stop the while loop from running
self.keep_going = False
rooms.py
#Remote imports
import pygame
#Local imports
import config
import continents
from game_components import *
from my_gui import *
from pgu import high
class Room(object):
def __init__(self, screen, ev_manager):
self.screen = screen
self.ev_manager = ev_manager
self.ev_manager.register_listener(self)
def notify(self, event):
if isinstance(event, TickEvent):
pygame.display.set_caption('FPS: ' + str(int(event.fps)))
self.render(self.screen)
pygame.display.update()
def get_highs_table(self):
fname = 'high_scores.txt'
highs_table = None
config.all_highs = high.Highs(fname)
if config.game_mode == config.TIME_CHALLENGE:
if config.difficulty == config.EASY:
highs_table = config.all_highs['time_challenge_easy']
if config.difficulty == config.MED_DIF:
highs_table = config.all_highs['time_challenge_med']
if config.difficulty == config.HARD:
highs_table = config.all_highs['time_challenge_hard']
if config.difficulty == config.SUPER:
highs_table = config.all_highs['time_challenge_super']
elif config.game_mode == config.PLAN_AHEAD:
pass
return highs_table
class TitleScreen(Room):
def __init__(self, screen, ev_manager):
Room.__init__(self, screen, ev_manager)
self.background = pygame.image.load('assets/images/interface/background.jpg').convert()
#Initialize
#---------------------------------------
self.gui_form = gui.Form()
self.gui_app = gui.App(config.gui_theme)
self.ev_manager.gui_app = self.gui_app
c = gui.Container(align=0,valign=0)
#Quit Button
#---------------------------------------
b = StartGameButton(ev_manager=self.ev_manager)
c.add(b, 0, 0)
self.gui_app.init(c)
def render(self, surface):
surface.blit(self.background, (0, 0))
#GUI
self.gui_app.paint(surface)
class GameModeRoom(Room):
def __init__(self, screen, ev_manager):
Room.__init__(self, screen, ev_manager)
self.background = pygame.image.load('assets/images/interface/background.jpg').convert()
self.create_gui()
#Create pgu gui elements
def create_gui(self):
#Setup
#---------------------------------------
self.gui_form = gui.Form()
self.gui_app = gui.App(config.gui_theme)
self.ev_manager.gui_app = self.gui_app
c = gui.Container(align=0,valign=-1)
#Mode Relaxed Button
#---------------------------------------
b = GameModeRelaxedButton(ev_manager=self.ev_manager)
self.b = b
print b.rect
c.add(b, 0, 200)
#Mode Time Challenge Button
#---------------------------------------
b = TimeChallengeButton(ev_manager=self.ev_manager)
self.b = b
print b.rect
c.add(b, 0, 250)
#Mode Think Ahead Button
#---------------------------------------
# b = PlanAheadButton(ev_manager=self.ev_manager)
# self.b = b
# print b.rect
# c.add(b, 0, 300)
#Initialize
#---------------------------------------
self.gui_app.init(c)
def render(self, surface):
surface.blit(self.background, (0, 0))
#GUI
self.gui_app.paint(surface)
class GameRoom(Room):
def __init__(self, screen, ev_manager):
Room.__init__(self, screen, ev_manager)
#Game mode
#---------------------------------------
self.new_board_timer = None
self.game_mode = config.game_mode
config.current_highs = self.get_highs_table()
self.highs_dialog = None
self.game_over = False
#Images
#---------------------------------------
self.background = pygame.image.load('assets/images/interface/game screen2-1.jpg').convert()
self.logo = pygame.image.load('assets/images/interface/logo_small.png').convert_alpha()
self.game_over_text = pygame.image.load('assets/images/interface/text_game_over.png').convert_alpha()
self.trip_complete_text = pygame.image.load('assets/images/interface/text_trip_complete.png').convert_alpha()
self.zoom_game_over = None
self.zoom_trip_complete = None
self.fade_out = None
#Text
#---------------------------------------
self.font = pygame.font.Font(config.font_sans, config.interface_font_size)
#Create game components
#---------------------------------------
self.continent = self.set_continent(config.continent)
self.board = Board(config.board_size, self.ev_manager)
self.deck = Deck(self.ev_manager, self.continent)
self.map = Map(self.continent)
self.longest_trip = 0
#Set pos of game components
#---------------------------------------
board_pos = (SCREEN_MARGIN[0], 109)
self.board.set_pos(board_pos)
map_pos = (config.screen_size[0] - self.map.size[0] - SCREEN_MARGIN[0], 57);
self.map.set_pos(map_pos)
#Trackers
#---------------------------------------
self.game_clock = Chrono(self.ev_manager)
self.swap_counter = 0
self.level = 0
#Create gui
#---------------------------------------
self.create_gui()
#Create initial board
#---------------------------------------
self.new_board = self.deck.deal_new_board(self.board)
self.ev_manager.post(NewBoardComplete(self.new_board))
def set_continent(self, continent_const):
#Set continent based on const
if continent_const == config.EUROPE:
return continents.Europe()
if continent_const == config.AFRICA:
return continents.Africa()
else:
raise Exception('Continent constant not recognized')
#Create pgu gui elements
def create_gui(self):
#Setup
#---------------------------------------
self.gui_form = gui.Form()
self.gui_app = gui.App(config.gui_theme)
self.ev_manager.gui_app = self.gui_app
c = gui.Container(align=-1,valign=-1)
#Timer Progress bar
#---------------------------------------
self.timer_bar = None
self.time_increase = None
self.minutes_left = None
self.seconds_left = None
self.timer_text = None
if self.game_mode == config.TIME_CHALLENGE:
self.time_increase = config.time_challenge_start_time
self.timer_bar = gui.ProgressBar(config.time_challenge_start_time,0,config.max_time_bank,width=306)
c.add(self.timer_bar, 172, 57)
#Connections Progress bar
#---------------------------------------
self.connections_bar = None
self.connections_bar = gui.ProgressBar(0,0,config.longest_trip_needed,width=306)
c.add(self.connections_bar, 172, 83)
#Quit Button
#---------------------------------------
b = QuitButton(ev_manager=self.ev_manager)
c.add(b, 950, 20)
#Generate Board Button
#---------------------------------------
b = GenerateBoardButton(ev_manager=self.ev_manager, room=self)
c.add(b, 500, 20)
#Board Size?
#---------------------------------------
bs = SetBoardSizeContainer(config.BOARD_LARGE, ev_manager=self.ev_manager, board=self.board)
c.add(bs, 640, 20)
#Fill Board?
#---------------------------------------
t = FillBoardCheckbox(config.fill_board, ev_manager=self.ev_manager)
c.add(t, 740, 20)
#Darkness?
#---------------------------------------
t = UseDarknessCheckbox(config.use_darkness, ev_manager=self.ev_manager)
c.add(t, 840, 20)
#Initialize
#---------------------------------------
self.gui_app.init(c)
def advance_level(self):
self.level += 1
print 'Advancing to next level'
print 'New level: ' + str(self.level)
if self.timer_bar:
print 'Time increase: ' + str(self.time_increase)
self.timer_bar.value += self.time_increase
self.time_increase = max(config.min_advance_time, int(self.time_increase * 0.9))
self.board = self.new_board
self.new_board = None
self.zoom_trip_complete = None
self.game_clock.unpause()
def notify(self, event):
#Tick event
if isinstance(event, TickEvent):
pygame.display.set_caption('FPS: ' + str(int(event.fps)))
self.render(self.screen)
pygame.display.update()
#Wait to deal new board when advancing levels
if self.zoom_trip_complete and self.zoom_trip_complete.finished:
self.zoom_trip_complete = None
self.ev_manager.post(UnfreezeCards())
self.new_board = self.deck.deal_new_board(self.board)
self.ev_manager.post(NewBoardComplete(self.new_board))
#New high score?
if self.zoom_game_over and self.zoom_game_over.finished and not self.highs_dialog:
if config.current_highs.check(self.level) != None:
self.zoom_game_over.visible = False
data = 'time:' + str(self.game_clock.time) + ',swaps:' + str(self.swap_counter)
self.highs_dialog = HighScoreDialog(score=self.level, data=data, ev_manager=self.ev_manager)
self.highs_dialog.open()
elif not self.fade_out:
self.fade_out = FadeOut(self.ev_manager, config.TITLE_SCREEN)
#Second event
elif isinstance(event, SecondEvent):
if self.timer_bar:
if not self.game_clock.paused:
self.timer_bar.value -= 1
if self.timer_bar.value <= 0 and not self.game_over:
self.ev_manager.post(GameOver())
self.minutes_left = self.timer_bar.value / 60
self.seconds_left = self.timer_bar.value % 60
if self.seconds_left < 10:
leading_zero = '0'
else:
leading_zero = ''
self.timer_text = ''.join(['Time Left: ', str(self.minutes_left), ':', leading_zero, str(self.seconds_left)])
#Game over
elif isinstance(event, GameOver):
self.game_over = True
self.zoom_game_over = ZoomImage(self.ev_manager, self.game_over_text)
#Trip complete event
elif isinstance(event, TripComplete):
print 'You did it!'
self.game_clock.pause()
self.zoom_trip_complete = ZoomImage(self.ev_manager, self.trip_complete_text)
self.new_board_timer = Timer(self.ev_manager, 2)
self.ev_manager.post(FreezeCards())
print 'Room posted newboardcomplete'
#Board Refresh Complete
elif isinstance(event, BoardRefreshComplete):
if event.board == self.board:
print 'Longest trip needed: ' + str(config.longest_trip_needed)
print 'Your longest trip: ' + str(self.board.longest_trip)
if self.board.longest_trip >= config.longest_trip_needed:
self.ev_manager.post(TripComplete())
elif event.board == self.new_board:
self.advance_level()
self.connections_bar.value = self.board.longest_trip
self.connection_text = ' '.join(['Connections:', str(self.board.longest_trip), '/', str(config.longest_trip_needed)])
#CardSwapComplete
elif isinstance(event, CardSwapComplete):
self.swap_counter += 1
elif isinstance(event, ConfigChangeBoardSize):
config.board_size = event.new_size
elif isinstance(event, ConfigChangeCardSize):
config.card_size = event.new_size
elif isinstance(event, ConfigChangeFillBoard):
config.fill_board = event.new_value
elif isinstance(event, ConfigChangeDarkness):
config.use_darkness = event.new_value
def render(self, surface):
#Background
surface.blit(self.background, (0, 0))
#Map
self.map.render(surface)
#Board
self.board.render(surface)
#Logo
surface.blit(self.logo, (10,10))
#Text
connection_text = self.font.render(self.connection_text, True, BLACK)
surface.blit(connection_text, (25, 84))
if self.timer_text:
timer_text = self.font.render(self.timer_text, True, BLACK)
surface.blit(timer_text, (25, 64))
#GUI
self.gui_app.paint(surface)
if self.zoom_trip_complete:
self.zoom_trip_complete.render(surface)
if self.zoom_game_over:
self.zoom_game_over.render(surface)
if self.fade_out:
self.fade_out.render(surface)
class HighScoresRoom(Room):
def __init__(self, screen, ev_manager):
Room.__init__(self, screen, ev_manager)
self.background = pygame.image.load('assets/images/interface/background.jpg').convert()
#Initialize
#---------------------------------------
self.gui_app = gui.App(config.gui_theme)
self.ev_manager.gui_app = self.gui_app
c = gui.Container(align=0,valign=0)
#High Scores Table
#---------------------------------------
hst = HighScoresTable()
c.add(hst, 0, 0)
self.gui_app.init(c)
def render(self, surface):
surface.blit(self.background, (0, 0))
#GUI
self.gui_app.paint(surface)
game_components.py
#Remote imports
import pygame
from pygame.locals import *
import random
import operator
from copy import copy
from math import sqrt, floor
#Local imports
import config
from events import *
from matrix import Matrix
from textrect import render_textrect, TextRectException
from hyphen import hyphenator
from textwrap2 import TextWrapper
##############################
#CONSTANTS
##############################
SCREEN_MARGIN = (10, 10)
#Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
YELLOW = (255, 200, 0)
#Directions
LEFT = -1
RIGHT = 1
UP = 2
DOWN = -2
#Cards
CARD_MARGIN = (10, 10)
CARD_PADDING = (2, 2)
#Card types
BLANK = 0
COUNTRY = 1
TRANSPORT = 2
#Transport types
PLANE = 0
TRAIN = 1
CAR = 2
SHIP = 3
class Timer(object):
def __init__(self, ev_manager, time_left):
self.ev_manager = ev_manager
self.ev_manager.register_listener(self)
self.time_left = time_left
self.paused = False
def __repr__(self):
return str(self.time_left)
def pause(self):
self.paused = True
def unpause(self):
self.paused = False
def notify(self, event):
#Pause Event
if isinstance(event, Pause):
self.pause()
#Unpause Event
elif isinstance(event, Unpause):
self.unpause()
#Second Event
elif isinstance(event, SecondEvent):
if not self.paused:
self.time_left -= 1
class Chrono(object):
def __init__(self, ev_manager, start_time=0):
self.ev_manager = ev_manager
self.ev_manager.register_listener(self)
self.time = start_time
self.paused = False
def __repr__(self):
return str(self.time_left)
def pause(self):
self.paused = True
def unpause(self):
self.paused = False
def notify(self, event):
#Pause Event
if isinstance(event, Pause):
self.pause()
#Unpause Event
elif isinstance(event, Unpause):
self.unpause()
#Second Event
elif isinstance(event, SecondEvent):
if not self.paused:
self.time += 1
class Map(object):
def __init__(self, continent):
self.map_image = pygame.image.load(continent.map).convert_alpha()
self.map_text = pygame.image.load(continent.map_text).convert_alpha()
self.pos = (0, 0)
self.set_color()
self.map_image = pygame.transform.smoothscale(self.map_image, config.map_size)
self.size = self.map_image.get_size()
def set_pos(self, pos):
self.pos = pos
def set_color(self):
image_pixel_array = pygame.PixelArray(self.map_image)
image_pixel_array.replace(config.GRAY1, config.COLOR1)
image_pixel_array.replace(config.GRAY2, config.COLOR2)
image_pixel_array.replace(config.GRAY3, config.COLOR3)
image_pixel_array.replace(config.GRAY4, config.COLOR4)
image_pixel_array.replace(config.GRAY5, config.COLOR5)