# 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 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.__fps.setPosition(0,0) 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)) 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))