Lots of computer games use buttons. An "old school" RPG (ie: pixel sprites and all), probably doesn't need mouse interaction, but I plan on making other game types too. And buttons are very very (ok, 100%) likely to be used. I know this because I've coded up a few new itterations already, one of which is getting surprisingly close to becoming a game. But I'll get to that later. For now - BUTTONS!
Buttons!
I'd like to appologize in advance, I wrote this code a couple of days ago, so I may forget to mention some of my alterations. I'll post the entire code though, so you should be able to find anything I miss. Although, so far so good, because it looks like the only change to myevents.py was the name of the manager class. I changed it to Mediator. If you check out my last post on design patterns, you'll find I have absolutely no clue if this is accurate or appropriate, but at least I don't have the confusion of "Manager" and "EventManager". However, since I see no reason to change a ton of code for just a name, the instance of the class is still "manager", as you will see.
I thought a bit about what buttons usually do, and came up with a list of possible statuses they could have. As you will see, in trying to keep with the same methodology as before, it becomes very easy to make this stuff more confusing than it needs to be. There will be a number of events to handle mouse activities. I decided to describe the button changes by noting the button status in the Button Event so that the view could react to all the possibilities. I came up with this list:
- hover
- cancel
- click
- press
- release
- enable
I added three events to handle mouse actions. Mouse button presses, releases and mouse movement each have an event. These are pretty standard.
The Button Model listens to all the mouse events. The only information it stores is ID, rect (position and size), and a boolean called ready. Again, this code is mediocre, IMO, but I didn't fix it up since I recognized that I'd be moving in a different direction. All the meat takes place in the notify method. The notify method handles the different mouse events to prepare for a button click. It is smart enough not to click if the user presses and holds the button, moves the mouse off, then releases. When the user clicks down on the button, the ready attribute is flagged to true, indicating that releasing the mouse button while still inside the button area means it should activate the button. Most of the other status possibilities are not even realized in this program - I abandoned it for a new version before implementing them. Anyway, the buttons send off a ButtonEvent, with the button attribute set to the button id, and the attribute "click" to true.
The Button View listens to both tick and button events. It requires a screen, button ID, and a rect to draw in. The color of the button is static since there's no reason to add that complexity yet. This view must listen to Tick Events or the button will not be drawn to the screen most of the time (remember that the ScreenView clears the screen with black every frame). So on tick events, the button is drawn to the screen (it does not show any changes when clicking or hovering over the button - again I hadn't gotten that far when I realized I didn't like where this was leading - but you will see this in an upcoming post). On button events, a message is printed to the console. Note that it doesn't even check what button constant was set. The whole process was feeling very manual and hard to use. There was no general way to make use of the extra information.
Finally, there is the Mouse Controller. This handy dandy listener goes through the pygame events every tick, and sends out the various mouse events. It's pretty straightforward, and hard to avoid needing this one.
That's it for new classes. As far as the old ones go - we'll only need a few. It's kind of worthwhile to note which classes are the most reusable. I left out all the old classes except for Screen View, Tick Generator Controller, and Quit Event. Notice that I never got around to using Quit Event in this version, but it's still an obviously useful event, so I left it in.
The main function is pretty simple, we create a Mediator named manager, a mouse controller, screen view, and a pair of button models and views. Then create and run the tick generator. The result is a black screen with two white buttons drawn on it. The visual is very very static (despite being redrawn every frame, since we never change it), but when you click on the top button the text "Button 1 has been clicked." in the console. The lower button says "Button 2 has been clicked.".
code
I'm including myevents.py for posterity - even though it is almost exactly the same as before.
from weakref import WeakKeyDictionary class Listener: """ A superclass for listeners. Note that a listener must save the reference to the manager if it needs to send events, as opposed to just recieve them. """ def __init__(self, manager): """Register with the manager automatically.""" manager.registerAll(self) def notify(self, event): """Default implementation for recieving events.""" pass class Event: """ A superclass for events to be handled by EventManager """ kind = "generic" def __init__(self): """Set the name of the Event. Subclasses may have other attributes.""" self.name = "Generic" self.sync = True def __repr__(self): """Custom representation for the event.""" return self.name + " Event" def attributes(self): """Return a formated string of the attributes""" result = '' for attr in self.__dict__.keys(): if attr[:2] == '__': result = result + "\tattr %s=\n" % attr else: result = result + "\tattr %s=%s\n" % (attr, self.__dict__[attr]) return result def hasAttribute(self, attr): """Check if the event has a given attribute""" return attr in self.__dict__.keys() def __getitem__(self, item): """Get the value of an attribute""" if item in self.__dict__.keys(): return self.__dict__[item] return None #def __getattr__(self, item): class TickEvent(Event): """ Tick events are intended to be time events. They dictate the pace of the program. """ kind = "tick" def __init__(self, time=None): Event.__init__(self) """Set the name and tick properties.""" self.name = "Tick" self.tick = time class EventManager: """ A manager for coordinating communication between Model, View and Controller """ def __init__(self): """Set up a dictionary to hold references to listeners""" self.listeners = WeakKeyDictionary() def register(self, listener): """Register a listener. It must implement the function notify(event)""" self.listeners[ listener ] = 1 def unregister(self, listener): """Unregister a listener""" if listener in self.listeners: del self.listeners[ listener ] def post(self, event): #print "posting " + event #print event.attributes() #print "to" + self.listeners.keys() for listener in self.listeners: listener.notify(event) class Mediator: """A collection of EventManagers, filtered by event type (kind).""" def __init__(self): self.managers = {} self.events = [] def register(self, clazz, listener): self.add(clazz) self.managers[clazz.kind].register(listener) def add(self, clazz): if not clazz.kind in self.managers: self.managers[clazz.kind] = EventManager() def drop(self, clazz): self.managers[clazz.kind] = None def registerAll(self, listener): """"Register for all managers - does nothing if a manager is added afterwards.""" for kind in self.managers: self.managers[kind].register(listener) def unregister(self, clazz, listener): if clazz.kind in self.managers: self.managers[clazz.kind].unregister(listener) def unregisterAll(self, listener): for kind in self.managers: self.managers[kind].unregister(listener) def post(self, event): if event.kind == TickEvent.kind: self.events.append(event) temp = self.events self.events = [] #print 'posting some events' for e in temp: #print e if e.kind in self.managers: self.managers[e.kind].post(e) else: if event.sync: self.events.append(event) else: #for m in self.managers: # self.managers[m].post(event) if event.kind in self.managers: self.managers[event.kind].post(event)
And here's the main shebang.
import pygame from myevents import * from pygame.locals import * class ButtonEvent(Event): kind = "button" CONSTANT = { 'HOVER':'hover', 'CANCEL':'cancel', 'CLICK':'click', 'PRESS':'press', 'RELEASE':'release', 'ENABLE':'enable' } def __init__(self, ID): Event.__init__(self) self.button = ID #def __setitem__(self, key, value): # #self.key = value # self.__dict__[key] = value class MouseMotionEvent(Event): kind = "mousemotion" def __init__(self, event): Event.__init__(self) self.name = "Mouse Motion" self.mousemotion = event class MouseButtonDownEvent(Event): kind = "mousedown" def __init__(self, event): Event.__init__(self) self.name = "Mouse Button Down" self.mousedown = event class MouseButtonUpEvent(Event): kind = "mouseup" def __init__(self, event): Event.__init__(self) self.name = "Mouse Button Up" self.mouseup = event class ButtonModel(Listener): def __init__(self, manager, ID, rect): self.id = ID self.manager = manager manager.register(MouseMotionEvent, self) manager.register(MouseButtonDownEvent, self) manager.register(MouseButtonUpEvent, self) #self.rect = rect self.rect = pygame.Rect( rect ) self.ready = False def notify(self, event): if event.kind == MouseButtonUpEvent.kind: if self.rect.collidepoint( event.mouseup.pos ) and self.ready: #self.manager.post( ButtonEvent(self.id) ) e = ButtonEvent(self.id) e[ ButtonEvent.CONSTANT.get('CLICK') ] = True self.ready = False if event.kind == MouseButtonDownEvent.kind: if self.rect.collidepoint( event.mousedown.pos ): #self.manager.post( ButtonEvent(self.id) ) self.ready = True if event.kind == MouseMotionEvent.kind: if self.rect.collidepoint( event.mousemotion.pos ): #print "Hovering over button "+str(self.id) pass class ButtonView(Listener): def __init__(self, manager, screen, ID, rect): manager.register(ButtonEvent, self) manager.register(TickEvent, self) self.screen = screen self.rect = rect #self.rect = pygame.Rect( rect ) self.id = ID def notify(self, event): if event.kind == TickEvent.kind: self.screen.fill( (255,255,255), self.rect) elif event.kind == ButtonEvent.kind and self.id == event.button: print "Button " +str(self.id)+ " has been clicked." class MouseController(Listener): def __init__(self, manager): self.manager = manager manager.register( TickEvent, self ) def notify(self, event): event = None for event in pygame.event.get(): if event.type == MOUSEBUTTONUP: self.manager.post( MouseButtonUpEvent(event) ) if event.type == MOUSEBUTTONDOWN: self.manager.post( MouseButtonDownEvent(event) ) if event.type == MOUSEMOTION: self.manager.post( MouseMotionEvent(event) ) # snagged from previous examples class ScreenView(Listener): def __init__(self, manager): pygame.init() manager.register(TickEvent, self) self.screen = pygame.display.set_mode( (400,500) ) self.screen.fill(black) def notify(self, event): pygame.display.flip() self.screen.fill(black) class TickGeneratorController(Listener): """ Uses a clock to generate a 30 FPS tick. Currently 30 is a magic number. Note that this class is a Listener, but there was really no need to extend the superclass. It is done here for consistency only. """ FPS = 50 def __init__(self, manager): self.manager = manager # we will be posting events self.manager.register(QuitEvent, self) self.running = True self.clock = pygame.time.Clock() def run(self): while self.running: self.manager.post( TickEvent(pygame.time.get_ticks()) ) self.clock.tick(self.FPS) # throttle back the CPU def notify(self, event): if event[QuitEvent.kind]: # if it is set to false - keep running self.running = False class QuitEvent(Event): kind = "quit" def __init__(self, quitter=True): Event.__init__(self) self.name = "Quit" self.quit = quitter self.sync = False black = (0,0,0) white = (255,255,255) def main(): manager = Mediator() mouse = MouseController(manager) screen = ScreenView(manager) button1model = ButtonModel( manager, 1, ( (50,50), (50,50) ) ) button1view = ButtonView( manager, screen.screen, button1model.id, button1model.rect ) button2model = ButtonModel( manager, 2, ( (50, 110), (50,50 ) ) ) button2view = ButtonView( manager, screen.screen, button2model.id, button2model.rect ) controller = TickGeneratorController(manager) controller.run() if __name__ == "__main__": main()
Labels: pygame, python, series 1, tutorial
Leave one.