Problem with room/screen/menu controller in python game: old rooms are not removed from memory
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
开发者_Go百科 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
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?
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()
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.
精彩评论