# 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))