please send comments / corrections via email to shandy AT geeky DOT net
I guess there's a lot of traffic hitting this right now, so I might as well mention that I'm looking for work. If anyone knows any company that's looking to hire a software developer, my resume is at resume.ezide.com
Last Update: Added another section to the networking part; January 11, 2004
This guide will not be dealing with such games. Some (perhaps all) of the ideas covered here may be applicable to Twitch games, but that is not the focus. This guide is most appliccable to games where it is acceptable to have a slight (if imperceptable) lag between user input and screen response.
Now, some might be thinking "Oh, this doesn't apply to me then, I want to make a Shooter / Real Time Strategy / etc.". However those kind of games might not be "Twitch" games. I recommend going ahead and trying to use this Guide. You'll have a better idea later if your game has such dire latency requirements.
We will start by trying to create a program where a little man moves around a grid of nine squares. This is a overly simple example, but easily extendable so we won't get tied up in the game rules, instead we can focus on the structure of the code.
We haven't even got to the Model yet, and already we have a difficulty. If you are familiar with using PyGame, you are probably used to seeing a main loop like this:
#stolen from the ChimpLineByLine example at pygame.org main(): ... while 1: #Handle Input Events for event in pygame.event.get(): if event.type == QUIT: return elif event.type == MOUSEBUTTONDOWN: fist.punch() elif event.type is MOUSEBUTTONUP: fist.unpunch() #Draw Everything allsprites.update() screen.blit(background, (0, 0)) allsprites.draw(screen) pygame.display.flip()
ControllerTick(): #Handle Input Events for event in pygame.event.get(): if event.type == QUIT: return 0 elif event.type == MOUSEBUTTONDOWN: fist.punch() elif event.type is MOUSEBUTTONUP: fist.unpunch() return 1 ViewTick(): #Draw Everything ... main(): ... while 1: if ControllerTick() == 0: return ViewTick()
Perhaps if time affects our game there will be even another Controller that sends messages every second, or perhaps there will be another View that spits text out to a log file. We now need to consider how we are going to handle multiple Views and Controllers. This leads us to the next pattern in our architecture, the Observer.
We implement the Observer pattern by creating an EventManager object. This middleman will allow multiple listeners to be notified when some other object changes state. Furthermore, that changing object doesn't need to know how many listeners there are, they can even be added and subtracted dynamically. All the changing object needs to do is send an Event to the EventManager when it changes.
If an object wants to listen for events, it must first register itself with the EventManager. We'll use the weakref WeakKeyDictionary so that listeners don't have to explicitly unregister themselves.
We will also need an Event class to encapsulate the events that can be sent via the EventManager.
class Event: """this is a superclass for any events that might be generated by an object and sent to the EventManager""" def __init__(self): self.name = "Generic Event" class EventManager: """this object is responsible for coordinating most communication between the Model, View, and Controller.""" def __init__(self ): from weakref import WeakKeyDictionary self.listeners = WeakKeyDictionary() #---------------------------------------------------------------------- def RegisterListener( self, listener ): self.listeners[ listener ] = 1 #---------------------------------------------------------------------- def UnregisterListener( self, listener ): if listener in self.listeners.keys(): del self.listeners[ listener ] #---------------------------------------------------------------------- def Post( self, event ): for listener in self.listeners.keys(): #NOTE: If the weakref has died, it will be #automatically removed, so we don't have #to worry about it. listener.Notify( event )
class KeyboardController: ... def Notify(self, event): if isinstance( event, TickEvent ): #Handle Input Events ... class CPUSpinnerController: ... def Run(self): while self.keepGoing: event = TickEvent() self.evManager.Post( event ) def Notify(self, event): if isinstance( event, QuitEvent ): self.keepGoing = 0 ... class PygameView: ... def Notify(self, event): if isinstance( event, TickEvent ): #Draw Everything ... main(): ... evManager = EventManager() keybd = KeyboardController() spinner = CPUSpinnerController() pygameView = PygameView() evManager.RegisterListener( keybd ) evManager.RegisterListener( spinner ) evManager.RegisterListener( pygameView ) spinner.Run()
For the purpose of this guide, we'll just use one kind of event, so every listener gets spammed with every event.
In our example, "little man" will be our sole Charactor.
In our example, the Map will be a discrete Map having a simple list of nine sectors.
In our example, we will allow no diagonal moves, only up, down, left and right. Each allowable move will be defined by the list of neighbors for a particular Sector, with the middle Sector having all four.
This example makes use of everything covered so far. It starts out with a list of possible events, then we define our middleman, EventManager, with all the methods we showed earlier.
Next we have our Controllers, KeyboardController and CPUSpinnerController. You'll notice keypresses no longer directly control some game object, instead they just generate events that are sent to the EventManager. Thus we have separated the Controller from the Model.
Next we have the parts of our PyGame View, SectorSprite, CharactorSprite, and PygameView. You'll notice that SectorSprite does keep a reference to a Sector object, part of our model. However we don't want to access any methods of this Sector object directly, we're just using it to identify which Sector object the SectorSprite object corresponds to. If we wanted to make this limitation more explicit we could use the id() function.
The Pygame View has a background group of green square sprites that represent the Sector objects, and a foreground group containing our "little man" or "red dot". It is updated on every TickEvent.
Finally we have the Model objects as discussed above and ultimately the main() function.
Here is a diagram of the major incoming and outgoing events.
The code in the following sections is written incrementally, so don't expect to just take the code from the first section and write a game with it. Subsequent sections sometimes address problems with the previously shown code and explain how to overcome those problems.
[TODO: intro to twisted]
Normally a server is something that runs as a daemon or in a text console; it does not have a graphical display. We can do this simply by replacing PygameView with a TextLogView as follows:
#------------------------------------------------------------------------------ class TextLogView: """...""" def __init__(self, evManager): self.evManager = evManager self.evManager.RegisterListener( self ) #---------------------------------------------------------------------- def Notify(self, event): if isinstance( event, CharactorPlaceEvent ): print event.name, " at ", event.charactor.sector elif isinstance( event, CharactorMoveEvent ): print event.name, " to ", event.charactor.sector elif not isinstance( event, TickEvent ): print event.name
Another thing we don't need in a server is keyboard input, so we can remove the KeyboardController. Where do input messages come from instead? They come from the network, so we'll need a Controller object for the messages sent by the clients, NetworkClientController.
from twisted.spread import pb #------------------------------------------------------------------------------ class NetworkClientController(pb.Root): """...""" def __init__(self, evManager): self.evManager = evManager self.evManager.RegisterListener( self ) #---------------------------------------------------------------------- def remote_GameStartRequest(self): ev = GameStartRequest( ) self.evManager.Post( ev ) return 1 #---------------------------------------------------------------------- def remote_CharactorMoveRequest(self, direction): ev = CharactorMoveRequest( direction ) self.evManager.Post( ev ) return 1 #---------------------------------------------------------------------- def Notify(self, event): pass
In our examples, we're only going to have one class in the server that is referenceable, and also only one class in the client. [TODO: expand on this]
Note: we also don't need the CPUSpinnerController in the server, so we've removed that. Previously, we used the Tick event to start the Game, now we'll need to explicitly start the game with our new GameStartRequest event.
class GameStartRequest(Event): def __init__(self): self.name = "Game Start Request"
def main(): """...""" evManager = EventManager() log = TextLogView( evManager ) clientController = NetworkClientController( evManager ) game = Game( evManager ) from twisted.internet.app import Application application = Application("myServer") application.listenTCP(8000, pb.BrokerFactory(clientController) ) application.run()
[TODO: make it more clear that this is merely a toy]
Here's an example of the server responding to a fake client.
$ python Python 2.2.1 (#1, Aug 30 2002, 12:15:30) [GCC 3.2 20020822 (Red Hat Linux Rawhide 3.2-4)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> from twisted.spread import pb >>> from twisted.internet import reactor >>> >>> class Connection: ... def connected(self, object): ... self.obj = object ... print "got object: ", self.obj ... >>> conn = Connection() >>> remoteResponse = pb.getObjectAt("localhost", 8000, 30) >>> remoteResponse.addCallbacks(conn.connected) <Deferred at 0x819b9fc> >>> reactor.iterate() >>> reactor.iterate() got object: <twisted.spread.pb.RemoteReference instance at 0x83964ec> >>> remoteResponse = conn.obj.callRemote("GameStartRequest") >>> up,down,left,right = 0,1,2,3 >>> remoteResponse = conn.obj.callRemote("CharactorMoveRequest", up) >>> remoteResponse = conn.obj.callRemote("CharactorMoveRequest", up) >>> remoteResponse = conn.obj.callRemote("CharactorMoveRequest", left) >>> remoteResponse = conn.obj.callRemote("CharactorMoveRequest", down) >>> remoteResponse = conn.obj.callRemote("CharactorMoveRequest", right) >>>Example of using the Python console as a fake client
$ python server1.py Map Finished Building Event Charactor Placement Event at <__main__.Sector instance at 0x81e9bec> Game Started Event Game Start Request Charactor Move Request Charactor Move Request Charactor Move Request Charactor Move Event to <__main__.Sector instance at 0x81e9e94> Charactor Move Request Charactor Move Event to <__main__.Sector instance at 0x81e9f6c> Charactor Move RequestRunning server1.py
The previous example of a server gave a good introduction to the basic networking technique, but it's a little too simple for our purposes. We don't really want to write a new function for every message the server can possibly receive. Instead, we'd like to leverage our already existing Event classes.
This brings us to one of the most important parts, but possibly the most tedious part of implementing networking. We need to go through all the possible events and answer these questions about each:
While there are many ways of doing this with Twisted, I will outline a strategy that tries to minimize the amount of code written (to combat the tediousness of this task) and to maintain the separation of the networking requirements from the remainder of the code.
Using Twisted, we must do three things to a class to make it possible to send it over the network: make it inherit from twisted.spread.pb.Copyable, make it inherit from twisted.spread.pb.RemoteCopy, and call twisted.spread.pb.setUnjellyableForClass() on it [TODO: ask someone who knows Twisted if that's really necessary]. Things can become even more complicated when we consider questions 4 and 5 from our list above -- does the data requires special formatting to send it over the network? The only data that doesn't require special formatting are the "built-in" types: int, string, list, tuple, and dict.
While examining the Events, two cases will occur, either it will not require reformatting, and we can just mix-in pb.Copyable and pb.RemoteCopy, or it will require reformatting and we will have to create a new class that has a routine to change the original data into something that can be sent over the network. [TODO: link to explain Mixins somewhere]
In this next example, we've split the code into multiple files. All the events are in events.py. In network.py, we try to answer all of the above questions for each event in events.py. If a message can go from the client to the server, we append it to the clientToServerEvents list, and likewise for the serverToClientEvents list. If the data in the event is simple, like integers and strings, then we can just mix-in the pb.Copyable and pb.RemoteCopy classes and call pb.setUnjellyableForClass() on the event.
# from network.py #------------------------------------------------------------------------------ # GameStartRequest # Direction: Client to Server only MixInCopyClasses( GameStartRequest ) pb.setUnjellyableForClass(GameStartRequest, GameStartRequest) clientToServerEvents.append( GameStartRequest ) #------------------------------------------------------------------------------ # CharactorMoveRequest # Direction: Client to Server only # this has an additional attribute, direction. it is an int, so it's safe MixInCopyClasses( CharactorMoveRequest ) pb.setUnjellyableForClass(CharactorMoveRequest, CharactorMoveRequest) clientToServerEvents.append( CharactorMoveRequest )
On the other hand, if an event contains data that is not network-friendly, like an object, we need to make a replacement event to send over the wire instead of the original. The simplest way to make a replacement is just to change any event attributes that were objects to unique integers using the id() function. This strategy requires us to keep a registry of objects and their ID numbers, so that when we recieve an event from the network referencing an object by its ID number, we can find the actual object.
# from network.py #------------------------------------------------------------------------------ # GameStartedEvent # Direction: Server to Client only class CopyableGameStartedEvent(pb.Copyable, pb.RemoteCopy): def __init__(self, event, registry): self.name = "Game Started Event" self.gameID = id(event.game) registry[self.gameID] = event.game pb.setUnjellyableForClass(CopyableGameStartedEvent, CopyableGameStartedEvent) serverToClientEvents.append( CopyableGameStartedEvent ) #------------------------------------------------------------------------------ # CharactorMoveEvent # Direction: Server to Client only class CopyableCharactorMoveEvent( pb.Copyable, pb.RemoteCopy): def __init__(self, event, registry ): self.name = "Charactor Move Event" self.charactorID = id( event.charactor ) registry[self.charactorID] = event.charactor pb.setUnjellyableForClass(CopyableCharactorMoveEvent, CopyableCharactorMoveEvent) serverToClientEvents.append( CopyableCharactorMoveEvent )
From the server, changes need to be sent out, so we need to create a new View on the server.
# from server2.py #------------------------------------------------------------------------------ class NetworkClientView: """We SEND events to the CLIENT through this object""" def __init__(self, evManager, sharedObjectRegistry): self.evManager = evManager self.evManager.RegisterListener( self ) self.clients = [] self.sharedObjs = sharedObjectRegistry #---------------------------------------------------------------------- def Notify(self, event): if isinstance( event, ClientConnectEvent ): self.clients.append( event.client ) ev = event #don't broadcast events that aren't Copyable if not isinstance( ev, pb.Copyable ): evName = ev.__class__.__name__ if not hasattr( network, "Copyable"+evName): return copyableClass = getattr( network, "Copyable"+evName) ev = copyableClass( ev, self.sharedObjs ) if ev.__class__ not in serverToClientEvents: return #NOTE: this is very "chatty". We could restrict # the number of clients notified in the future for client in self.clients: remoteCall = client.callRemote("ServerEvent", ev)
NetworkClientView.Notify() is primarily interested in Copyable events. The event passed in to Notify() might already be Copyable, due to the mixing in of pb.Copyable in network.py. In that case, isinstance( ev, pb.Copyable ) returns true. If it's not Copyable, there still might be a replacement class in the network module, and we can check by prepending "Copyable" to the event's class name because we used that naming convention for the replacement classes in network.py.
As can be seen in NetworkClientView.Notify(), the server expects the client to send it a remotely accessible object (like one that inherits from Twisted's pb.Root) when the client connects. Thereafter, the server can use that object to notify the client of events.
Now we'll (finally) get started on the client. From the point of view of the client, the incoming messages from the server represent a Controller, so we've got a NetworkServerController class in client2.py. As you might be expecting, the client will also send events to the server through a View, the NetworkServerView.
from client2.py #------------------------------------------------------------------------------ class NetworkServerView(pb.Root): """We SEND events to the server through this object""" ... #---------------------------------------------------------------------- def Connected(self, server): self.server = server self.state = NetworkServerView.STATE_CONNECTED ev = ServerConnectEvent( server ) self.evManager.Post( ev ) ... #---------------------------------------------------------------------- def Notify(self, event): ev = event if isinstance( event, TickEvent ) \ and self.state == NetworkServerView.STATE_PREPARING: self.state = NetworkServerView.STATE_CONNECTING remoteResponse = pb.getObjectAt("localhost", 8000, 30) remoteResponse.addCallback(self.Connected ) ...
from client2.py #------------------------------------------------------------------------------ class NetworkServerController(pb.Referenceable): """We RECEIVE events from the server through this object""" def __init__(self, evManager, twistedReactor): self.evManager = evManager self.evManager.RegisterListener( self ) self.reactor = twistedReactor #---------------------------------------------------------------------- def remote_ServerEvent(self, event): self.evManager.Post( event ) return 1 #---------------------------------------------------------------------- def Notify(self, event): if isinstance( event, ServerConnectEvent ): #tell the server that we're listening to it and #it can access this object event.server.callRemote("ClientConnect", self) if isinstance( event, TickEvent ): #PUMPING NETWORK self.reactor.iterate()
We will create a PhonyModel on the client side who's state we will keep in sync with the authoritative model on the server.
Here's the tricky part: how do we send complex objects like Players or Charactors over the channel we've created? This is called serialization. To serialize our objects, we need to do two things.
When events referencing complex objects get to the NetworkClientView on the server, the objects are serialized starting in the constructor of the Copyable event.
#from server3.py class NetworkClientView: ... def Notify(self, event): ... ev = event if not isinstance( ev, pb.Copyable ): evName = ev.__class__.__name__ if not hasattr( network, "Copyable"+evName): return copyableClass = getattr( network, "Copyable"+evName) if copyableClass not in serverToClientEvents: return #It is here that serialization starts ev = copyableClass( ev, self.sharedObjs ) elif ev.__class__ not in serverToClientEvents: return for client in self.clients: self.RemoteCall( client, "ServerEvent", ev )
#from network.py class CopyableCharactorMoveEvent( pb.Copyable, pb.RemoteCopy): def __init__(self, event, registry ): self.name = "Copyable " + event.name self.charactorID = id( event.charactor ) registry[self.charactorID] = event.charactor
When the client is sent the CopyableCharactorMoveEvent, the PhonyModel picks it up (the PhonyModel is the only object interested in events that start with "Copyable").
#from client.py #---------------------------------------------------------------------- def Notify(self, event): ... if isinstance( event, CopyableCharactorMoveEvent ): charactorID = event.charactorID if self.sharedObjs.has_key(charactorID): charactor = self.sharedObjs[charactorID] self.CharactorMoveCallback( charactor ) else: charactor = self.game.players[0].charactors[0] self.sharedObjs[charactorID] = charactor remoteResponse = self.server.callRemote("GetObjectState", charactorID) remoteResponse.addCallback(self.ObjStateReturned, self.CharactorMoveCallback)
GetObjectState() basically just finds that object on the server (in this example, the Charactor that has moved), and serializes it's data with a call to getStateToCopy(). GetObjectState() returns the dict and the object ID that was requested.
#from network.py #------------------------------------------------------------------------------ class CopyableCharactor: def getStateToCopy(self, registry): d = self.__dict__.copy() del d['evManager'] if self.sector is None: sID = None else: sID = id( self.sector ) d['sector'] = sID registry[sID] = self.sector return d def setCopyableState(self, stateDict, registry): neededObjIDs = [] success = 1 if stateDict['sector'] == None: self.sector = None elif not registry.has_key( stateDict['sector'] ): registry[stateDict['sector']] = Sector(self.evManager) neededObjIDs.append( stateDict['sector'] ) success = 0 else: self.sector = registry[stateDict['sector']] return [success, neededObjIDs]
The client receives this information in the ObjStateReturned() function, which is somewhat difficult to follow. In our example, the arguments passed to ObjStateReturned() are the response from the GetObjectState() call to the server ( [charactorID, charactorDict] ), and the desired final callback CharactorMoveCallback() from the client.
#from client.py -- this is where the "nextFn" argument comes from def Notify(self, event): ... remoteResponse = self.server.callRemote("GetObjectState", charactorID) remoteResponse.addCallback(self.ObjStateReturned, self.CharactorMoveCallback)
#from server.py -- this is where the "response" argument comes from def remote_GetObjectState(self, objectID): ... return [objectID, objDict]
#from client.py #---------------------------------------------------------------------- def ObjStateReturned(self, response, nextFn=None): """this is a callback that is called in response to invoking GetObjectState on the server""" if response[0] == 0: print "GOT ZERO -TODO: better error handler here" return None objID = response[0] objDict = response[1] obj = self.sharedObjs[objID] retval = obj.setCopyableState(objDict, self.sharedObjs) if retval[0] == 1: #we successfully set the state and no further objects #are needed to complete the current object if objID in self.neededObjects: self.neededObjects.remove(objID) else: #to complete the current object, we need to grab the #state from some more objects on the server. The IDs #for those needed objects were passed back in retval[1] for neededObjID in retval[1]: if neededObjID not in self.neededObjects: self.neededObjects.append(neededObjID) self.waitingObjectStack.append( (obj, objDict, nextFn) ) self.GetAllNeededObjects()
#from client.py #---------------------------------------------------------------------- def GetAllNeededObjects(self): if len(self.neededObjects) == 0: #this is the recursion-ending condition. If there are #no more objects needed to be grabbed from the server #then we can try to setCopyableState on them again and #we should now have all the needed objects, ensuring #that setCopyableState succeeds while self.waitingObjectStack: t = self.waitingObjectStack.pop() obj = t[0] objDict = t[1] fn = t[2] retval = obj.setCopyableState(objDict, self.sharedObjs) if retval[0] == 0: print "WEIRD!! - TODO:better Error here" if fn: fn( obj ) return #still in the recursion step. Try to get the object state for #the objectID on the end of the stack. Note that the recursion #is done via a deferred, which may be confusing nextID = self.neededObjects[len(self.neededObjects)-1] remoteResponse = self.server.callRemote("GetObjectState", nextID) remoteResponse.addCallback(self.ObjStateReturned)
Here is a flowchart that summarizes the actions taken when the client gets an event containing a complex object.
Notice that we must make sure that the event we send over the network has enough information to update the client with any relevant changes to the state of the server. The client may already have a local version of an object, but if that object has changed, the client still has to call GetObjectState(), as is demonstrated with the CharactorMoveEvent.
With that in mind, a question is raised: where do we put the intelligence do determine what object states we need to retrieve? Right now, we've put all this logic in PhonyModel.Notify() [TODO: is this the best place? what about inside Copyable events?]
[TODO: more details esp. regarding PhonyModel]
The previous discussion is a good start and provides some usefull code. I encourage you to play around with it and see if you can get your game sending objects back and forth. As your code becomes more complex, you will run into some more problems:
To clarify, here's an example of when an issue like this might come up. Lets say we write a game where two Penguins fight each other. Each Penguin has a weapon, and every weapon is initialized with a name, like "Deathbringer" or "Destroy-o-Matic", or "Patricia".
#------------------------------------------------------------------------------ class Weapon: def __init__( self, evManager, name ) self.evManager = evManager self.name = name
CopyablePenguin would thus look something like this:
#------------------------------------------------------------------------------ class CopyablePenguin: def getStateToCopy(self, registry): d = self.__dict__.copy() del d['evManager'] wID = id( self.weapon ) registry[wID] = self.weapon d['weapon'] = wID return d
def setCopyableState(self, stateDict, registry): neededObjIDs = [] success = 1 wID = stateDict['weapon'] if not registry.has_key( wID ): #registry didn't have the object, so create a new one self.weapon = Weapon( self.evManager, #WELL CRAP! I don't yet know what its name is, so how am I going to initialize it?
... wID = stateDict['weapon'] if not registry.has_key( wID ): #registry didn't have the object, so create a new one self.weapon = ??? #MORE CRAP! I don't even know what class of object it should be!
We can solve this problem with a Placeholder object that is very similar to the Lazy Proxy design pattern.
... [TODO: finish this section]
- Player no longer generated by Game.__init__() - Player comes from external source - Player must be uniquely identifiable to the server - Queue needed for EventManager - Convention: "Request" events are NEVER generated inside the model, always generated by the UI[TODO]
But here you may notice that Player One's client can control Player Two's charactor. This is obviously not desireable. We want the server to reject any request where the player instance contained in the request is not an instance created by that client. One way to solve this problem is for the the client to create a random number or password when it sends a PlayerJoinRequest. Then, in every subsequent client-generated event that references a player, it includes that password. The server can store the passwords as they come in, and check that they match for every event that references a player instance.
There is another way, and it is conveniently built into Twisted. We'll take a look at it in the next chapter. [TODO ... or not]
You can get as complicated as you like when creating your GUI engine, but this tutorial will focus only on some simple widgets. Here are the ones we will implement:
#------------------------------------------------------------------------------ class Widget(pygame.sprite.Sprite): def __init__(self, evManager, container=None): pygame.sprite.Sprite.__init__(self) self.evManager = evManager self.evManager.RegisterListener( self ) self.container = container self.focused = 0 self.dirty = 1 #---------------------------------------------------------------------- def SetFocus(self, val): self.focused = val self.dirty = 1 #---------------------------------------------------------------------- def kill(self): self.container = None del self.container pygame.sprite.Sprite.kill(self) #---------------------------------------------------------------------- def Notify(self, event): if isinstance( event, GUIFocusThisWidgetEvent ) \ and event.widget is self: self.SetFocus(1) elif isinstance( event, GUIFocusThisWidgetEvent ) \ and self.focused: self.SetFocus(0)
#------------------------------------------------------------------------------ class LabelSprite(Widget): def __init__(self, evManager, text, container=None): Widget.__init__( self, evManager, container) self.color = (200,200,200) self.font = pygame.font.Font(None, 30) self.__text = text self.image = self.font.render( self.__text, 1, self.color) self.rect = self.image.get_rect() #---------------------------------------------------------------------- def SetText(self, text): self.__text = text self.dirty = 1 #---------------------------------------------------------------------- def update(self): if not self.dirty: return self.image = self.font.render( self.__text, 1, self.color ) self.dirty = 0
#------------------------------------------------------------------------------ class ButtonSprite(Widget): def __init__(self, evManager, text, container=None, onClickEvent=None ): Widget.__init__( self, evManager, container) self.font = pygame.font.Font(None, 30) self.text = text self.image = self.font.render( self.text, 1, (255,0,0)) self.rect = self.image.get_rect() self.onClickEvent = onClickEvent #---------------------------------------------------------------------- def update(self): if not self.dirty: return if self.focused: color = (255,255,0) else: color = (255,0,0) self.image = self.font.render( self.text, 1, color) #self.rect = self.image.get_rect() self.dirty = 0 #---------------------------------------------------------------------- def Connect(self, eventDict): for key,event in eventDict.iteritems(): try: self.__setattr__( key, event ) except AttributeError: print "Couldn't connect the ", key pass #---------------------------------------------------------------------- def Click(self): self.dirty = 1 if self.onClickEvent: self.evManager.Post( self.onClickEvent ) #---------------------------------------------------------------------- def Notify(self, event): if isinstance( event, GUIPressEvent ) and self.focused: self.Click() elif isinstance( event, GUIClickEvent ) \ and self.rect.collidepoint( event.pos ): self.Click() elif isinstance( event, GUIMouseMoveEvent ) \ and self.rect.collidepoint( event.pos ): ev = GUIFocusThisWidgetEvent(self) self.evManager.Post( ev ) Widget.Notify(self,event)
#------------------------------------------------------------------------------ class TextBoxSprite(Widget): def __init__(self, evManager, width, container=None ): Widget.__init__( self, evManager, container) self.font = pygame.font.Font(None, 30) linesize = self.font.get_linesize() self.rect = pygame.Rect( (0,0,width, linesize +4) ) boxImg = pygame.Surface( self.rect.size ).convert_alpha() color = (0,0,100) pygame.draw.rect( boxImg, color, self.rect, 4 ) self.emptyImg = boxImg.convert_alpha() self.image = boxImg self.text = '' self.textPos = (22, 2) #---------------------------------------------------------------------- def update(self): if not self.dirty: return text = self.text if self.focused: text += '|' textColor = (255,0,0) textImg = self.font.render( text, 1, textColor ) self.image.blit( self.emptyImg, (0,0) ) self.image.blit( textImg, self.textPos ) self.dirty = 0 #---------------------------------------------------------------------- def Click(self): self.focused = 1 self.dirty = 1 #---------------------------------------------------------------------- def SetText(self, newText): self.text = newText self.dirty = 1 #---------------------------------------------------------------------- def Notify(self, event): if isinstance( event, GUIPressEvent ) and self.focused: self.Click() elif isinstance( event, GUIClickEvent ) \ and self.rect.collidepoint( event.pos ): self.Click() elif isinstance( event, GUIClickEvent ) \ and self.focused: self.SetFocus(0) elif isinstance( event, GUIMouseMoveEvent ) \ and self.rect.collidepoint( event.pos ): ev = GUIFocusThisWidgetEvent(self) self.evManager.Post( ev ) elif isinstance( event, GUIKeyEvent ) \ and self.focused: newText = self.text + event.key self.SetText( newText ) elif isinstance( event, GUIControlKeyEvent ) \ and self.focused and event.key == K_BACKSPACE: #strip of last character newText = self.text[:( len(self.text) - 1 )] self.SetText( newText ) Widget.Notify(self,event)
Above is a diagram showing some common uses of a Graphical User Interface in games. In each of the above screens, there is a blue section representing buttons or other widgets. It will also serve as the idea for our next example application, "Fool The Bar".
from module import *
? Don't you know
that's bad coding style