# FortuneEngine is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# FortuneEngine is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with the FortuneEngine. If not, see .
#
# Author: Justin Lewis
import pygame
from time import time
from GameEngineConsole import GameEngineConsole
from GameInspect import GameInspect
from DrawableObject import DrawableObject
from DynamicDrawableObject import DynamicDrawableObject
from DrawableFontObject import DrawableFontObject
from Scene import Scene
class GameEngine(object):
"""
The Fortune Engine GameEngine is a main loop wrapper around pygame.
It manages the event and drawing loops allowing the user to just
register for user events and drawing time in the draw loop.
"""
instance = None
def __init__(self, width=1200, height=900, always_draw=False, fps_cap=15):
"""
Constructor for the game engine.
@param width: Window width
@param height: Window height
@param always_draw: Boolean to set the animation mode to always
draw vs draw when set_dirty is called
@param fps_cap: Sets the framerate cap. Set to 0 to disable
the cap. Warning: setting the cap to 0 when
always draw = False will cause cpu 100% when
not driving.
"""
GameEngine.instance = self
pygame.init()
pygame.mouse.set_visible(False)
# Window Settings
self.width = width
self.height = height
size = width, height
self.screen = pygame.display.set_mode(size)
self.__fps = DrawableFontObject("", pygame.font.Font(None, 17))
self.__scene = Scene(self.__fps)
# Engine Internal Variables
self.__fps_cap = fps_cap
self.__showfps = False
self.__dirty = True
self.__always_draw = always_draw
self.__font = pygame.font.Font(None, 17)
self.__run_event = False
# Variables to hold game engine elements and callbacks
self.__event_cb = []
self.__draw_lst = []
self.__object_hold = {}
self.__dirtyList=[]
# Game Timers
self.__active_event_timers = []
self.__active_event_timers_tick = []
# Game Clock
self.clock = pygame.time.Clock()
self.__tick_time = 0
# Inspector
self._inspector = GameInspect(self.__object_hold)
# Time Profiler Timers
self.__draw_time = {}
self.__draw_calls = {}
self.__event_time = {}
self.__event_calls = {}
self.__timer_time = {}
self.__timer_calls = {}
# Initialize Py Console
self.console = GameEngineConsole(self, (0, 0, width, height / 2))
# Disable Mouse Usage
# TODO Allow mouse motion on request
pygame.event.set_blocked(pygame.MOUSEMOTION)
def set_dirty(self):
"""
Sets the dirty flag to force the engine to draw the next time
it enters the draw flag.
"""
self.__dirty = True
def get_scene(self):
return self.__scene
def start_event_timer(self, function_cb, time):
"""
Starts a timer that fires a user event into the queue every "time"
milliseconds
@param function_cb: The function to call when timer fires
@param time: Milliseconds between fires
"""
avail_timer = len(self.__active_event_timers)
if avail_timer + pygame.USEREVENT < pygame.NUMEVENTS:
if function_cb not in self.__active_event_timers:
self.__timer_time[str(function_cb)] = 0
self.__timer_calls[str(function_cb)] = 0
self.__active_event_timers.append(function_cb)
self.__active_event_timers_tick.append(time)
pygame.time.set_timer(pygame.USEREVENT + avail_timer, time)
else:
print "ERROR TIMER IN LIST"
else:
print "Ran out of timers :("
self.stop_event_loop()
def stop_event_timer(self, function_cb):
"""
Stops the timer that has id from firing
@param function_cb: The function registered with the timer that
should be canceled
"""
try:
timer_id = self.__active_event_timers.index(function_cb)
except ValueError:
return
pygame.time.set_timer(pygame.USEREVENT + timer_id, 0)
del self.__active_event_timers[timer_id]
del self.__active_event_timers_tick[timer_id]
# Timers have been removed, now need to clear any events
# already fired and in the queue
pygame.event.clear(pygame.USEREVENT + timer_id)
def list_event_timers(self):
"""
returns a list of configured timers, if the timers has a time of 0 the
timer is disabled
"""
timer_list = "Event Timers:\n"
i = 0
for timer_item in self.__active_event_timers:
timer_list += "\t%d: %d\n" % (timer_item,
self.__active_event_timers_tick[i])
i = i + 1
return timer_list
def list_timer_time(self):
"""
Returns a string representation of the time the game spends
in each timer callback.
"""
mystr = "Timer Times:\n\tName\tCalls\tTotal Time\tAvg"
for key in self.__timer_time:
timer_times = self.__timer_time[key]
timer_calls = self.__timer_calls[key]
if timer_calls == 0:
avg = 0
else:
avg = timer_times / timer_calls
mystr = "%s\n\t%s\n\t\t%d\t%f\t%f" % \
(mystr, key, timer_calls, timer_times, avg)
return mystr
def start_main_loop(self):
"""
Starts the game loop.
This function does not return until after the game loop exits
"""
self.__run_event = True
self._event_loop()
def _draw(self, tick_time):
"""
Draws all elements in draw callback to the screen
@param tick_time: The amount of time passed since last
draw cycle. (should be produced by
pygamme.clock.tick method)
"""
screen = self.screen
# If console is active, we want to draw console, pausing
# game drawing (events are still being fired, just no
# draw updates.
if self.console.active:
self.console.draw()
pygame.display.flip()
else:
for fnc in self.__draw_lst:
start = time()
fnc(screen, tick_time)
self.__draw_time[str(fnc)] += time() - start
self.__draw_calls[str(fnc)] += 1
# Print Frame Rate
if self.__showfps:
self.__fps.changeText('FPS: %d' % self.clock.get_fps(), (255,255,255))
self.__fps.setPosition(0,0)
else:
self.__fps.changeText('')
self.__scene.update(tick_time)
pygame.display.update(self.__scene.draw(screen))
def _event_loop(self):
"""
The main event loop.
"""
while self.__run_event:
event = pygame.event.poll()
# Handle Game Quit Message
if event.type == pygame.QUIT:
self.__run_event = False
# No-Op sent, draw if set to always draw
elif event.type == pygame.NOEVENT:
# Tick even if not drawing
# We want to pause the cpu from getting into a
# 100% usage looping on the poll until something
# becomes dirty
self.__tick_time += self.clock.tick(self.__fps_cap)
if self.__always_draw or self.__dirty:
self.__dirty = False
self._draw(self.__tick_time)
self.__tick_time = 0
# Handle User event Timers
elif event.type >= pygame.USEREVENT and \
event.type < pygame.NUMEVENTS:
timer_id = event.type - pygame.USEREVENT
# Call timer
str_rep = str(self.__active_event_timers[timer_id])
start = time()
self.__active_event_timers[timer_id]()
self.__timer_time[str_rep] += time() - start
self.__timer_calls[str_rep] += 1
# Check if we should activate the console
elif event.type == pygame.KEYDOWN and event.key == pygame.K_w \
and pygame.key.get_mods() & pygame.KMOD_CTRL:
self.console.set_active()
self.set_dirty()
# Pass event to console
elif self.console.process_input(event):
self.set_dirty()
# Pass events to all others
else:
# Make a copy first so that adding events don't get fired
# right away
list_cp = self.__event_cb[:]
# Reverse list so that newest stuff is on top
# TODO: cache this list
list_cp.reverse()
for cb in list_cp:
# Fire the event for all in cb and stop
# if the callback returns True
start = time()
retur_val = cb(event)
self.__event_time[str(cb)] += time() - start
self.__event_calls[str(cb)] += 1
if retur_val:
break
def stop_event_loop(self):
"""
Sends a pygame.QUIT event into the event queue which
exits the event loop
"""
pygame.event.post(pygame.event.Event(pygame.QUIT))
def add_event_callback(self, cb):
"""
Adds event callback to the event callback stack
@param cb: Callback to be added to the stack when events are fired.
"""
self.__event_time[str(cb)] = 0
self.__event_calls[str(cb)] = 0
self.__event_cb.append(cb)
def remove_event_callback(self, cb):
"""
Removes an event from the event callback stack
@param cb: The callback to remove from the event callback stack
@return: Returns true if successful in removing callback
"""
try:
self.__event_cb.remove(cb)
return True
except:
return False
def list_event_callbacks(self):
"""
Returns a string representation of all events registered with the game
engine
"""
event_callbacks = "Event Listeners:\n"
for eventlst in self.__event_cb:
event_callbacks = "%s\t%s\n" % (event_callbacks, str(eventlst))
return event_callbacks
def list_event_time(self):
"""
Returns a string representation of the time the game spends
in each event callback.
"""
mystr = "Event Times:\n\tName\tCalls\tTotal Time\tAvg"
for key in self.__event_time:
event_times = self.__event_time[key]
event_calls = self.__event_calls[key]
if event_calls == 0:
avg = 0
else:
avg = event_times / event_calls
mystr = "%s\n\t%s\n\t\t%d\t%f\t%f" % \
(mystr, key, event_calls, event_times, avg)
return mystr
def add_draw_callback(self, fnc):
"""
Adds a callback to the draw list. Function will be passed the
game screen it should draw too.
@param fnc: The function to call when system is drawing
"""
self.__draw_time[str(fnc)] = 0
self.__draw_calls[str(fnc)] = 0
self.__draw_lst.append(fnc)
def pop_draw_callback(self):
"""
Removes top of draw stack and returns it
@return: Returns the top callback function that was removed
"""
return self.__draw_lst.pop()
def clear_draw_callback(self):
"""
Empties draw callback stack
"""
self.__draw_lst = []
def remove_draw_callback(self, fnc):
"""
Removes a draw callback from the game engine draw function
@param fnc: The callback function to remove
@return: Returns true if successful removal of the function
"""
try:
self.__draw_lst.remove(fnc)
return True
except:
return False
def list_draw_callbacks(self):
"""
Lists all the draw callbacks currently registered with the game engine
"""
callbacks = "Draw Callbacks:\n"
for eventlst in self.__draw_lst:
callbacks += "\t%s\n" % str(eventlst)
return callbacks
def list_draw_time(self):
"""
Returns a string representation of the time the game spends
in each drawing callback.
"""
mystr = "Drawing Times:\n\tName\tCalls\tTotal Time\tAvg"
for key in self.__draw_time:
draw_times = self.__draw_time[key]
draw_calls = self.__draw_calls[key]
if draw_calls == 0:
avg = 0
else:
avg = draw_times / draw_calls
mystr = "%s\n\t%s\n\t\t%d\t%f\t%f" % \
(mystr, key, draw_calls, draw_times, avg)
return mystr
def has_object(self, name):
"""
Returns true if object is stored in game engine
@param name: Name of the object to check if exists
@return: Returns true if object found
"""
return name in self.__object_hold
def add_object(self, name, obj):
"""
Adds an object to the game engine datastore
@param name: The name used to store the object
@param obj: The object to store
"""
self.__object_hold[name] = obj
def get_object(self, name):
"""
Returns an object from the game engine datastore
@param name: The name of object to return
@return: Returns the object
"""
return self.__object_hold[name]
def remove_object(self, name):
"""
Removes an object from the game engine datastore
@param name: The name of the object to remove
"""
del self.__object_hold[name]
def list_objects(self):
"""
Returns a sting of registered objects
"""
objlist = "Objects Registered:\n"
for eventlst in self.__object_hold:
objlist += "\t%s\n" % str(eventlst)
return objlist
def toggle_fps(self):
"""
Toggles fps display
"""
self.__showfps = not self.__showfps
def art_scale(self, original, expected, width=True):
if width:
return int(self.width / float(expected) * float(original))
else:
return int(self.height / float(expected) * float(original))