views:

74

answers:

3

I'm literally banging my head against a wall here (as in, yes, physically, at my current location, I am damaging my cranium). Basically, I've got a Python/Pygame game with some typical game "rooms", or "screens." EG title screen, high scores screen, and the actual game room. Something bad is happening when I switch between rooms: the old room (and its various items) are not removed from memory, or from my event listener. Not only that, but every time I go back to a certain room, my number of event listeners increases, as well as the RAM being consumed! (So if I go back and forth between the title screen and the "game room", for instance, the number of event listeners and the memory usage just keep going up and up.

The main issue is that all the event listeners start to add up and really drain the CPU. I'm new to Python, and don't know if I'm doing something obviously wrong here, or what.

I will love you so much if you can help me with this!

Below is the relevant source code. Complete source code at http://www.necessarygames.com/my_games/betraveled/betraveled_src0328.zip (Requires Python 2.6 + Pygame 1.9)

MAIN.PY

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)

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



EVENT_MANAGER.PY

class EventManager:

    #This object is responsible for coordinating most communication
    #between the Model, View, and Controller.
    def __init__(self):
        from weakref import WeakKeyDictionary
        self.last_listeners = {}
        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 clear(self):
        del self.listeners[:]

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

        print 'Number of listeners: ' + str(len(self.listeners))

        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)

    def notify(self, event):
        pass

#------------------------------------------------------------------------------
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) or isinstance(incoming_event, BoardCreationTick):

            #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.MOUSEBUTTONDOWN and not self.input_freeze:
                    if event.button == 2:    #Button 2
                        pos = pygame.mouse.get_pos()
                        ev = MouseButtonRightEvent(pos)   
                elif event.type == pygame.MOUSEBUTTONUP and not self.input_freeze:
                    if event.button == 2:    #Button 2 Release
                        pos = pygame.mouse.get_pos()
                        ev = MouseButtonRightReleaseEvent(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)        

#        elif isinstance(event, BoardCreationTick):
#            #Share some time with other processes, so we don't hog the cpu
#            pygame.time.wait(5)               
#                           
#            #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)

#------------------------------------------------------------------------------            
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(fps=fps))

        pygame.quit()


    #----------------------------------------------------------------------
    def notify(self, event):
        if isinstance(event, QuitEvent):
            #this will stop the while loop from running
            self.keep_going = False       



EXAMPLE CLASS USING EVENT MANAGER

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   
+1  A: 

When you do something like this:

return rooms.TitleScreen(self.screen, self.ev_manager) 

I'm assuming that you're creating a new TitleScreen object.

If this is what you want to do then you probably want to delete the old room object when switching rooms.

def notify(self, event):
  if isinstance(event, ChangeRoomRequest):
    if event.game_mode:
      config.game_mode = event.game_mode            
    del self.room  // delete the old room object
    self.room = self.set_room(event.new_room)

If you want the rooms to persist, your set_room function is going to be have to check to see if the room has already been created. Then you can create a new room or load the old one intelligently. But you will also have to keep track of these rooms somehow.

EDIT:

OK then. The problem isn't the rooms, it's listeners. Every listener you register on init should probably be unregistered on del. I did a search for 'unregister_listener' in your src and only found it unregistering the room listeners.

So when you create 100 buttons and then create 100 more without unregistering any listeners, you'll have 100 orphaned listeners. That's not good. I'd overload the __ del __ () function to remove those listeners just like the __ init __ () function adds them.

Does that make sense?

jjfine
Thanks for the answer jjfine. Unfortunately I've tried that exact code (and tried it again now), but it doesn't solve the problem. The room itself disappears from the listeners, but all of the room's properties that were registered with the event listener are still there.I've also tried "loading the old one intelligently," but that hasn't worked either... I'll post the code I've tried below as an answer.
Jordan Magnuson
Check my edit...
jjfine
Thanks jjfine, I'll try that. Did you notice that my listener dictionary is a weak ref dictionary, though? I'm confused, because I thought that was supposed to get rid of weak references automatically, so I assumed it would get rid of stuff when they weren't being referenced any more, which is why I wasn't explicitly unregistering listeners.
Jordan Magnuson
A: 

I've also tried keeping track of the rooms, instead of throwing them and creating new ones each time a new room request comes around. Unfortunately, this code isn't working for me either: when I return to the title screen from the game room, the title screen renders, but nothing responds to anything... I'm not sure what the problem is there...

Watching the number of listeners, this doesn't seem to be solving the "creep" problem, as it goes from: 1) Title Screen: 4 listeners active, to: 2) Game mode select screen: 5 listeners active, to: 3) Game room: 86 listeners active, to: 4) Title Screen (unresponsive): 100 listeners active

Here's the code I tried in MAIN.PY

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.title_screen = None
        self.game_mode_room = None
        self.game_room = None
        self.high_scores_room = None

        self.room = self.set_room(config.room)

    def set_room(self, room_const):
        #Set new room based on const
        if room_const == config.TITLE_SCREEN:
            if self.title_screen == None:
                self.title_screen = rooms.TitleScreen(self.screen, self.ev_manager)
            return self.title_screen
        elif room_const == config.GAME_MODE_ROOM:
            if self.game_mode_room == None:
                self.game_mode_room = rooms.GameModeRoom(self.screen, self.ev_manager) 
            return self.game_mode_room        
        elif room_const == config.GAME_ROOM:
            if self.game_room == None:
                self.game_room = rooms.GameRoom(self.screen, self.ev_manager)
            return self.game_room
        elif room_const == config.HIGH_SCORES_ROOM:
            if self.high_scores_room == None:
                self.high_scores_room = rooms.HighScoresRoom(self.screen, self.ev_manager)
            return self.high_scores_room 

    def notify(self, event):
        if isinstance(event, TickEvent):        
            self.render(self.screen) 
            pygame.display.update()
        elif isinstance(event, SecondEvent):
            pygame.display.set_caption(''.join(['FPS: ', str(int(event.fps))]))          
        elif 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)

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__": 
#    cProfile.run('main()', 'cprofile')
    main()
Jordan Magnuson
A: 

Well, temporarily "solved" this by adding a clear() function to my event manager, and calling that before every room switch, clearing out all listeners except for my three controllers:

def clear(self):    
    for listener in list(self.listeners):
        if not isinstance(listener, CPUSpinnerController):  
            if not isinstance(listener, RoomController):  
                if not isinstance(listener, PyGameEventController):  
                    self.unregister_listener(listener)

Doesn't seem like the best method though. If anyone has any insight as to why this isn't working, or why my event listener has to be manually cleared even though I'm using a weak ref dictionary to hold the listeners, I'd love to hear it.

Jordan Magnuson