sjbrown's Writing Games Tutorial

By Shandy Brown. Please send comments / corrections via email to sjbrown AT geeky DOT net

Last Update: May 2008


Table Of Contents

What You Should Know

This guide assumes a certain level of knowledge. If you find it confusing, either you should brush up on some of these concepts, or I should become a better writer. It kind of puts us into an arms race of laziness.

Object Oriented Programming

It is expected the reader is comfortable in an object oriented environment. All important parts are encapsulated into classes.

Design Patterns

This guide makes use of the Design Patterns "Model View Controller" (MVC) and "Mediator". If this sounds foreign to you, I reccommend checking out the book "Design Patterns" by Gamma et al. or just surfing the web for tutorials. You may be able to follow along without having previous exposure to Design Patterns, as their purpose is quickly evident to people familiar with object oriented programming.

PART 1

Example Goal

It's always a good idea to sketch out your game either with pictures or text before you begin coding.

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.

example applicaton

The Architecture

Model View Controller

The choice of MVC should be pretty obvious where a graphical game is concerned. The primary Model will be discussed later under the heading The Game Model. The primary View will be a PyGame window displaying graphics on the monitor. The primary Controller will be the keyboard, supported by PyGame's internal pygame.event module.

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 == MOUSEBUTTONUP:
                fist.unpunch()

        #Draw Everything
        allsprites.update()
        screen.blit(background, (0, 0))
        allsprites.draw(screen)
        pygame.display.flip()
In this example, the Controller (the "Handle Input Events" part) and the View (the "Draw Everything" part) are tightly coupled, and this is generally how PyGame works, at every iteration of the main loop, it is expected that we will check for input events, update all the visible sprites, and redraw the screen. However the MVC pattern requires the View and the Controller to be separate. Our solution is to introduce a Tick() function that the constantly looping main loop can call for both the View and the Controller. That way there will not be View-specific code in the same location as Controller-specific code. Here is a rough example:
 ControllerTick():
    #Handle Input Events
    for event in pygame.event.get():
        if event.type == QUIT:
            return False
        elif event.type == MOUSEBUTTONDOWN:
            fist.punch()
        elif event.type == MOUSEBUTTONUP:
            fist.unpunch()
    return True

 ViewTick():
    #Draw Everything
    ...

 main():
    ...
    while 1:

        if not ControllerTick():
            return

        ViewTick()
Here is some more info on the MVC pattern: MVC @ Wikipedia MVC @ ootips.org

Rationale

Readers with some experience writing games may be balking at this point, thinking MVC is more complex than necessary, and that it will add unneeded overhead, especially when the goal is to create a simple, arcade-style game. Now historically, arcade games were just that, games written for arcade machines. The code ran "close to the metal", and would squeeze all the resources of the machine just to get a 3-color ghost to flash blue every other frame. In the 21st century, we have resource-rich personal computers where applications run a couple layers above the metal. Hence, organizing your code into a pattern has a small relative cost. For that small cost, you get the following advantages: more easily add networking, easily add new views (file loggers, radars, multiple zoom levels, ...), keep the Model code "cleaner" by decoupling it from the view and the controller, and I contend, more readable code.

Mediator

Let's examine the infinite while loop in the last bit of code. What is its job? It basically sends the Tick() message out to the View and the Controller as fast as the CPU can manage. In that sense it can be viewed as a piece of hardware sending messages into the program, just like the keyboard; it can be considered another Controller.

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 Mediator.

architecture

We implement the Mediator 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 removed 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 ):
        """Post a new event.  It will be broadcast to all listeners"""
        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 )
Here is a rough idea how this might be integrated with the previous code.
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 = False
            ...


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

Diversion: Event Types and Selective Listeners

As we get more and more listeners, we may find that it's inefficient to spam every listener with every event. Perhaps some listeners only care about certain events. One way to make things more efficient is to classify the events into different groups.

For the purpose of this guide, we'll just use one kind of event, so every listener gets spammed with every event.

Advanced Event Managers

If you try to use this particular Event Manager class for your own project, you might notice it has some shortcomings. In particular, if a block of code generates events A and B sequentially, and a listener catches event A and generates event C, the above Event Manager class will process the events in the order A,C,B, instead of the desired order of A,B,C. In Part 3, we will see an example of a more advanced Event Manager.
Here is some more info on the Mediator pattern and the related Observer pattern: Mediator @ Wikipedia Observer @ ootips.org

The Game Model

Creating a model, we need to go through a process called "abstraction". We have played games before, so we have a valuable mental library of concrete examples of finished products similar, in principle, to the finished product we want to create. If we can find abstract commonalities in those concrete examples, it will help us create classes to organize our code, make it flexible, make it maintainable and give us a vocabulary to talk to other team members about the code. There are many possible abstractions we can come up with and judging whether we have created good abstraction is very subjective. It's important to keep your goals in mind and also anticipate how the requirements could possibly change in the future.

Here is a Model that has worked for me and is general enough to adapt to many types of games:

example applicaton

Game

Game is mainly a container object. It contains the Players and the Maps. It might also do things like Start() and Finish() and keep track of whose turn it is.

Player

A Player object represents the actual human (or computer) that is playing the game. Common attributes are Player.score and Player.color. Don't confuse it with Charactor. Pac Man is a Charactor, the person holding the joystick is a Player.

Charactor

A Charactor is something controlled by a player that moves around the Map. Synonyms might be "Unit" or "Avatar". It is intentionally spelled "Charactor" to avoid any ambiguity with Character which can also mean "a single letter" (also, you cannot create a table in PostgreSQL named "Character"). Common Charactor attributes are Charactor.health and Charactor.speed.

In our example, "little man" will be our sole Charactor.

Map

A Map is an area that Charactors can move around in. There are generally two kinds of maps, discrete ones that have Sectors, and continuous ones that have Locations. A chess board is an example of a discrete map. A 3-dimensional level in Quake (with floating-point precision), or a level in Super Mario (with pixel-precision) are examples of continuous Maps.

In our example, the Map will be a discrete Map having a simple list of nine sectors.

Sector

A Sector is part of a Map. It is adjacent to other sectors of the map, and might have a list of any such neighbors. No Charactor can logically be in between Sectors. If a Charactor is in a Sector, it is in that sector entirely, and not in any other Sector (I'm speaking functionally here. It can look like it is in between Sectors, but that is an issue for the View, not the Model)

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.

Location

We won't get into Locations of a continuous Map, as they don't apply to our example.

Item

You'll notice that in the figure, Item is not explicitly connected to anything. This is left up to the developer. You could have a design constraint that Items must be contained by Charactors (perhaps in an intermidiate "Inventory" object), or maybe it makes more sense for your game to keep a list of a bunch of Items in the Game object. Some games might call for Sectors having Items lying around inside them.

Our Example

example applicaton

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.

example incoming messages example outgoing messages

PART 2

Internet Play

Our next task will be to make the game playable over the internet. Eventually this will lead to multiplayer capability for our game, but it's important that we do the single-player network step first, as it exposes us to several constraints that may affect any future code.

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.

Structure of Network Hosts

Computer processes (usually there is only one process of interest on each physical computer, or "host", so we just use the term "host") that communicate over the network can be organized in many ways. In games there are three popular ways to structure hosts, Peer-to-Peer, "Strict" Client-Server, and "Servent" Client-Server. The driving factor when deciding how to structure network hosts for games is usually trust. Games are emotional (well, good ones) and competetive, players are motivated to win, and when they don't win, they want to trust that no other player had an unfair advantage. To ensure trust, there needs to be a consistent, authoritative model.

In the "Strict" Client-Server structure, there is one "3rd party" server that all the clients connect to. Any change to the authoritative game model must happen at the server. A client can predict the authoritative state, but it must not put faith in game state until it hears from the server that that is, in fact the case. An example game would be World of Warcraft.

Client-Server @ Wikipedia

In the "Servent" Client-Server structure, one of the players, usually the one that starts the game, acts as the server as well. This suffers from the drawback that other players trust the game state as much as they trust that particular player. However no 3rd party is needed. Examples can be found in many first person shooter games. This structure is often paired with a 3rd party "matching" server that connects players with each other and then hands off to the Servent host.

Servent @ Wikipedia

In the Peer to Peer structure, all hosts have identical roles. The great benefit of a Peer to Peer structure is that it robustly deals with network disconnects from individual hosts. However trust is compromised. Trust can be bolstered by adopting token passing strategy such that the host holding the token acts as a Servent.

Peer to Peer @ Wikipedia

For our examples, we will examine the "Strict" Client-Server structure.

Synchronus / Asynchronus

When you call a function that requires network communication, it may take a long time to finish. If we waited for network-dependent functions to finish before we called the functions which draw the graphics, the users would get angry and flame us on internet message boards. The solution is to write functions that send out messages over the network and then return immediately, not waiting for a reply. The replies will eventually come from the remote hosts and wait in a queue until our process can service them. It is important to remember that the replies may not queue up in the same order as the requests went out.

This asynchronus quality is fundamental to network-related code. Luckily designing our code such that there is an independent EventManager and well-defined events will make dealing with asynchronus messages from the network fairly painless.

This tutorial will use the Twisted framework for network-related code. I recommend reading the Twisted documentation, though it should not be necessary to get through this tutorial. (note, a lot of the Twisted documentation focuses on writing servers where the client implementation is unknown. I recommend skipping forward to the sections on Perspective Brokers) The ideas presented here should be independent from the choice of Twisted; the examples could just as well be implemented with raw sockets or carrier pigeons.

Twisted is a framework that hides the queue from us, it expects the programmer to call reactor.run(), which is a mainloop that consumes the queue and fires off callbacks. The callbacks are provided by the programmer.

Implementation

Example Server

For the server, we'll start with the exact same code as before. Just rename example1.py to server1.py.

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
We are already reaping the benefits of the MVC pattern. By changing only a small amount of code, we no longer have a Pygame display, instead the TextLogView just prints received events out to the console.

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
The NetworkClientController instance is a special object that can be sent across the network via Twisted's Perspective Broker mechanism (because it inherits from pb.Root). The remote client will request a reference to the NetworkClientController instance, once it has received it, it can call any method that starts with "remote_". So for the client to send messages to the server, we have implemented remote_GameStartRequest and remote_CharactorMoveRequest.

Caveat

It could be tempting to make all of the objects remotely referenceable. (ie, inherit from pb.Referenceable) The problem with that approach is that it tightly couples the networking code with the rest of the code. It's preferable to separate the networking code so that the other objects just use the event passing strategy described by the Mediator pattern.

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]

We also don't need the CPUSpinnerController in the server, so we've removed that, and replaced it with Twisted's reactor, which similarly provides a run() method.

def main():
    evManager = EventManager()

    log = TextLogView( evManager )
    clientController = NetworkClientController( evManager )
    game = Game( evManager )
    
    from twisted.internet import reactor

    reactor.listenTCP( 8000, pb.PBServerFactory(clientController) )

    reactor.run()

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"
It is not necessary to understand the Twisted parts of this, you can just consider them "magic". What you should know is that invoking reactor.run() causes the mainloop to block while listening on port 8000.

If we play some dirty tricks, we can see what our server does without writing a client. Instead, we will just connect to it using the Python interactive interpreter. Now, reactor.run() is a blocking call that does not return until the reactor is shut down, so in order to get back to the interactive prompt, we have to crash the reactor and then call reactor.iterate() in order to communicate with it. It should go without saying that this is not a recommended practice. Also, if you replicate the session below, you may have to call iterate() multiple times before you see any result.

 $ python
Python 2.5.2 (r252:60911, Apr 21 2008, 11:17:30) 
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu7)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from twisted.spread import pb
>>> from twisted.internet import reactor
>>> factory = pb.PBClientFactory()
>>> server = None
>>> def gotServer(serv):
...     global server
...     server = serv
... 
>>> connection = reactor.connectTCP('localhost', 8000, factory)
>>> reactor.callLater( 4, reactor.crash )
<twisted.internet.base.DelayedCall instance at 0xac5638>
>>> reactor.run()
>>> d = factory.getRootObject()
>>> d.addCallback(gotServer)
<Deferred at 0xb1f440  current result: None>
>>> reactor.iterate()
>>> server.callRemote('GameStartRequest')
<Deferred at 0xac5638>
>>> reactor.iterate()
>>> up, right, down, left = 0,1,2,3
>>> server.callRemote('CharactorMoveRequest', up)
<Deferred at 0xb1f4d0>
>>> reactor.iterate()
>>> server.callRemote('CharactorMoveRequest', right)
<Deferred at 0xac5638>
>>> reactor.iterate()
>>> server.callRemote('CharactorMoveRequest', down)
<Deferred at 0xb1f4d0>
>>> reactor.iterate()
>>> server.callRemote('CharactorMoveRequest', left)
<Deferred at 0xac5638>
>>> reactor.iterate()
Example of using the Python console as a fake client
 $ python server1.py 
Game Start Request
Map Finished Building Event
Game Started Event
Charactor Placement Event  at  <__main__.Sector instance at 0xc9b290>
Charactor Move Request
Charactor Move Request
Charactor Move Event  to  <__main__.Sector instance at 0xc9b320>
Charactor Move Request
Charactor Move Event  to  <__main__.Sector instance at 0xc9b290>
Charactor Move Request
Charactor Move Event  to  <__main__.Sector instance at 0xc9b3b0>
Running server1.py

Note that the request to move up did not result in a Move event.

 

We can fake the client in a more proper way by using a tool that comes with Twisted, twisted.conch.stdio. We just start a python interpreter with this module and then we can omit the reactor abuse:

 $ python -m twisted.conch.stdio
>>> from twisted.spread import pb
>>> from twisted.internet import reactor
>>> 
>>> factory = pb.PBClientFactory()
>>> server = None
>>> 
>>> def gotServer(serv):
...     global server
...     server = serv
... 
>>> connection = reactor.connectTCP('localhost', 8000, factory)
>>> d = factory.getRootObject()
>>> d.addCallback(gotServer)
<Deferred at 0xc227a0  current result: None>
>>> server.callRemote('GameStartRequest')
<Deferred #0>
Deferred #0 called back: 1
>>> up, right, down, left = 0,1,2,3
>>> server.callRemote('CharactorMoveRequest', up)
<Deferred #1>
Deferred #1 called back: 1
>>> server.callRemote('CharactorMoveRequest', right)
<Deferred #2>
Deferred #2 called back: 1
>>> server.callRemote('CharactorMoveRequest', down)
<Deferred #3>
Deferred #3 called back: 1
>>> server.callRemote('CharactorMoveRequest', left)
<Deferred #4>
Deferred #4 called back: 1
Using twisted.conch.stdio as a fake client

 

King of the Castle

As seen above, Twisted's reactor object is designed to be in charge of the main loop. This creates some difficulty, as we've already got a main loop in the CPUSpinnerController. We could subordinate Twisted's reactor, and "pump" it on every iteration of the CPUSpinnerController's main loop, but that has the drawback that we need to abuse Twisted's API in a way that was probably not intended, and may not be forward-compatible.
# Example of a class that pumps a Twisted reactor
class ReactorSlaveController(object):
    def __init__(self):
        ...
        factory = pb.PBClientFactory()
        self.reactor = SelectReactor()
        installReactor(self.reactor)
        connection = self.reactor.connectTCP('localhost', 8000, factory)
        self.reactor.startRunning()
        ...

    def PumpReactor(self):
        self.reactor.runUntilCurrent()
        self.reactor.doIteration(0)

    def Stop(self):
        self.reactor.addSystemEventTrigger('after', 'shutdown',
                                            self.onReactorStop)
        self.reactor.stop()
        self.reactor.run() #excrete anything left in the reactor

    def onReactorStop(self):
        '''This gets called when the reactor is absolutely finished'''
        self.reactor = None
Alternatively, we can use Twisted in the intended way and then use the LoopingCall class to make the firing of the Tick event reliant on the reactor's main loop. Creating a LoopingCall object is a way of asking the reactor to call a function repeatedly. The downside of this approach is that games often start out in single-player mode and we don't want to invoke any network-related code like Twisted unless the user chooses a multiplayer option.
# Example of using LoopingCall to fire the Tick event
from twisted.internet.task import LoopingCall

...

def FireTick(evManager):
    evManager.Post( TickEvent() )

loopingCall = LoopingCall(FireTick, evManager)
interval = 1.0 / FRAMES_PER_SECOND
loopingCall.start(interval)
Ultimately, the choice is up to you. You should weigh the pros and cons of each approach based on the type of game you are writing. In the examples, we will use the reactor-pumping approach.

Messages Over the Wire

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:

  1. Do we need to send it from the client to the server?
  2. Do we need to send it from the server to the client?
  3. Are there security issues with sending this data over the network?
  4. Is the data formatted in a way that it can be sent over the network?
  5. If we must, how do we reformat the data so that it can be sent?
(Eventually, one might also ask "How often will this message be sent?" and therefore "How can I best optimise this message?")

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 instances of 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 require special formatting to send it over the network? The only data that doesn't require special formatting are the literal types: string, int, float, etc., None, and containers (lists, tuples, dicts) thereof.

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 )
It is very important that these classes are named exactly the same as the class they're replacing but with a prefix of "Copyable". We can see how to replace the original events with these network-friendly versions in NetworkClientView.Notify in server.py and we can see how the receipt of these events is handled in PhonyModel.Notify in client.py.

Creating A Communication Channel

We've seen that we can send fake messages to the server via an interactive python shell, but what we really want is a graphical client. There are a few steps to realizing this goal. Firstly, the client(s) need to be notified of any changes to the state of the server. So we'll need bidirectional communication. Not only does the client send requests to the server, but the server also notifies the client of events. (This is why one-way ("pull") protocols like XML-RPC or HTTP are not well suited to our needs)

From the server, changes need to be sent out, so we need to create a new View on the server.

# from server.py

#------------------------------------------------------------------------------
class NetworkClientView(object):
	"""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__
			copyableClsName = "Copyable"+evName
			if not hasattr( network, copyableClsName ):
				return
			copyableClass = getattr( network, copyableClsName )
			ev = copyableClass( ev, self.sharedObjs )

		if ev.__class__ not in network.serverToClientEvents:
			#print "SERVER NOT SENDING: " +str(ev)
			return

		#NOTE: this is very "chatty".  We could restrict 
		#      the number of clients notified in the future
		for client in self.clients:
			print "=====server sending: ", str(ev)
			remoteCall = client.callRemote("ServerEvent", ev)

The NetworkClientView keeps a reference to the server's registry that maps object ID numbers to the actual objects. It also has a list of clients. The objects in the clients list inherit from pb.Referenceable, so we can use the callRemote() method, sending messages over the network. The serverToClientEvents list is imported from network.py.

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 client.py. As you might be expecting, the client will also send events to the server through a View, the NetworkServerView.

# from client.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 AttemptConnection(self):
                ...
                connection = self.reactor.connectTCP(serverHost, serverPort,
                                                     self.pbClientFactory)
                deferred = self.pbClientFactory.getRootObject()
                deferred.addCallback(self.Connected)
                deferred.addErrback(self.ConnectFailed)
                self.reactor.startRunning()

        ...

        #----------------------------------------------------------------------
        def Notify(self, event):
                ev = event

                if isinstance( event, TickEvent ):
                        if self.state == NetworkServerView.STATE_PREPARING:
                                self.AttemptConnection()
                        ...
On the first TickEvent that the NetworkServerView gets, it attempts to connect to the server. When the connection is made, the Connected() method is called with a reference to a server object that inherits from pb.Referenceable, therefore the client can use it to remotely access the server. It also creates a ServerConnectEvent.
# from client.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 )

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

The NetworkServerController gets notified of that ServerConnectEvent and uses it to pass the server a reference to itself. Now the server can call the remote_ServerEvent() method of the NetworkServerController. So both the server and the client have references to remotely callable objects. This is the channel through which they communicate.
example applicaton

Sending Complex Objects

Theoretically, there should be only one model, the authoritative model on the server, and the clients should just be Views and Controllers for that model. This is not feasible in practice, however. For one, it is no simple matter to keep references to remote model objects in the client's EventManager, Views and Controllers. Another reason is that many events can be handled entirely on the client side, and always sending them to the server would create needless noise.

We will create a PhonyModel on the client side whose 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.

The unique IDs will be the result of the id() function called on the object in question on the server. It must originate from the server so that it is unique, otherwise we'd have multiple IDs for a single object.

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 server.py

class NetworkClientView:
        ...

        def Notify(self, event):
                ...

                ev = event

                if not isinstance( ev, pb.Copyable ):
                        evName = ev.__class__.__name__
                        copyableClsName = "Copyable"+evName
                        if not hasattr( network, copyableClsName )
                                return
                        copyableClass = getattr( network, copyableClsName )
                        #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 )
Let's take a CharactorMoveEvent as an example. The above code will call __init__() for CopyableCharactorMoveEvent.
# 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
As you can see, the server won't send the actual object when it sends the event, it will only send a unique integer ID. It also makes sure that there is a mapping from that ID to the actual object in the registry.

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").

Warning: from here on down, the tutorial discusses the version of Twisted that was available 3 years ago. I will update it when I get some free time

-sjbrown (May 2008)

#from client.py

class PhonyModel
        ...

        #----------------------------------------------------------------------
        def Notify(self, event):
                ...

                if isinstance( event, CopyableCharactorMoveEvent ):
                        charactorID = event.charactorID
                        if self.sharedObjs.has_key(charactorID):
                                charactor = self.sharedObjs[charactorID]
                        else:
                                charactor = self.game.players[0].charactors[0]
                                self.sharedObjs[charactorID] = charactor
                        remoteResponse = self.server.callRemote("GetObjectState", charactorID)
                        remoteResponse.addCallback(self.StateReturned)
                        remoteResponse.addCallback(self.CharactorMoveCallback)
When a charactor moves, he is in a new sector. To communicate this to the client, the server sends a CharactorMoveEvent, which has one attribute, the charactor himself. The client receives this event, sees the charactor referenced inside, and requests the new state (which sector is he in?) for that charactor.

This is a very generic approach to solving the problem.

We could have just hand-crafted the CopyableCharactorMoveEvent into something more specific to our game's needs, for example we could have included the sector as an attribute of the event to obviate the request for more information. But we'll keep the code very generic for now. A lot of the other events will follow the same pattern.

Back to the code snippet, if the client has already received that object from the server, self.sharedObjs.has_key() will return true, and it can grab a reference to the object from the registry and carry on as normal. If it hasn't received that object yet (as is the case the first time this event is received), it must first create a placeholder object, and then copy the state of the object on the server into this new placeholder object. It does this by calling GetObjectState() with the unique ID of the needed object.

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']
                sID = id( self.sector )
                d['sector'] = sID
                registry[sID] = self.sector
                return d

        def setCopyableState(self, stateDict, registry):
                neededObjIDs = []
                success = 1
                if 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 dict that getStateToCopy() returns contains all network-friendly data, so it can be sent over the network.

The client receives this information in the StateReturned() function, which is probably the most difficult function to follow in this whole tutorial. I'll try to go through it step-by-step.

The client first requests the Object state. When the response comes, the callbacks StateReturned and CharactorMoveCallback are queued to be called in sequence.

# from client.py

        def Notify(self, event):
                ...

                        remoteResponse = self.server.callRemote("GetObjectState", charactorID)
                        remoteResponse.addCallback(self.StateReturned)
                        remoteResponse.addCallback(self.CharactorMoveCallback)
The first callback, StateReturned will be called with [objectID, objDict] as its "response" argument.
# from server.py

        def remote_GetObjectState(self, objectID):
                ...

                return [objectID, objDict]
# from client.py

        #----------------------------------------------------------------------
        def StateReturned(self, response):
                """this is a callback that is called in response to 
                invoking GetObjectState on the server"""

                objID, objDict = response
                if objID == 0:
                        print "GOT ZERO -- better error handler here"
                        return None
                obj = self.sharedObjs[objID]

                success, neededObjIDs =\
                                 obj.setCopyableState(objDict, self.sharedObjs)
                if success:
                        #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) )

                retval = self.GetAllNeededObjects()
                if retval:
                        # retval is a Deferred - returning it causes a chain
                        # to be formed.
                        return retval
In the simplest case, "success" is True, and GetAllNeededObjects() returns None immediately. Then the next callback, CharactorMoveCallback gets called, and we're done.

However, if "success" was False, that means more data is needed to complete the originally requested object's state. The PhonyModel keeps a list of neededObjects that must be requested from the server before the originally requested object is complete. Each of these needed objects may also append to the neededObjects list for subsequent objects they need. So when we call GetAllNeededObjects() the recursive behaviour begins.

# 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
                        return self.ConsumeWaitingObjectStack()

                #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[-1]
                remoteResponse = self.server.callRemote("GetObjectState",nextID)
                remoteResponse.addCallback(self.StateReturned)
                return remoteResponse

As you can see, another call is made to GetObjctState on the server that will result in StateReturned being called. Notice that this isn't truly recursive. GetAllNeededObjects doesn't block. It returns immediately. But it returns a Deferred object, remoteResponse. So the original Deferred had it's first callback called, and that returned a new Deferred object. This is called Chaining Deferreds and it causes the first callback to block until the second Deferred's callbacks are finished. Hence we get recursion over the network.

Here is a flowchart that summarizes the actions taken when the client gets an event containing a complex object.

flowchart of client event reception

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]

More Problems

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:

  1. What if we don't have enough information to call __init__ for some attributes in setCopyableState() ?
  2. What if we don't know the specific subclass for an attribute in setCopyableState() ?

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
We would naievely start writing the corresponding setCopyableState function:
    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?
Furthermore, lets say that Weapons are of one of three subclasses, either Slingshot, Rifle, or Nuke. Then we have even more difficulties with setCopyableState:
        ...
        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]

Reconnecting After A Drop

As is wont with the internet, sometimes a connection gets accidentally dropped. [TODO: more details]

Multiplayer

Non-Networked Multiplayer

We'll start off by creating a 2-player game that runs locally, not over the network. Our loosely coupled architecture allows us to do this, and it's a great advantage to be able to develop your ideas first and worry about network issues later.
- 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]

Networked Multiplayer

[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]

PART 3

Graphical User Interface

What Is A Widget

A widget is the elemental object in a GUI. A widget can be a button, a label, a text entry field, etc. A widget can even contain other widgets, like a toolbar, or a menubar, or even a simple horizontal box.

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:

All widgets share a small amount of behaviour, so we have an abstract Widget class that inherits from Sprite. Widgets can be focused and unfocused, and have a 'dirty' flag so that they can be redrawn when needed, and not on every single call to update().
#------------------------------------------------------------------------------
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)

Label

Probably the simplest widget is a label. It is basically just a holder for some text.
#------------------------------------------------------------------------------
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

Button

A button is also a very simple widget. It is just an image that can be clicked, and when it is clicked, it fires off an event. For simplicity's sake the image is just some rendered text, but it could be anything.
#------------------------------------------------------------------------------
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)

Text Box

A text box is a little bit more complicated but still easy to understand. It is basically a rectangle into which text can be typed. When it gets focus it shows a little vertical bar (|) and starts responding to keypress events.
#------------------------------------------------------------------------------
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)

GUI Screens

the visualstages of an example game

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

Menu

menu example The Menu GUI is the first thing seen when the program starts up. It is usually just a collection of buttons, most often things like "New Game", "Quit" and "Options". Sometimes there are multiple kinds of "Options" choices.

Options

options example The Options GUI is where the user sets their preferences or adds their personal information. It is usually text labels with adjacent fields where the user can change / add the values.

Main

main example The Main GUI is where the game actually gets played. Few games have much in common when it comes to the Main GUI. However, many games have a "function bar" or a "shortcut bar" along some edge that is made up of buttons or other widgets.

Cutscene

cutscene example A Cutscene is part of the game where direct control is taken away and part of the game's story is presented. This can be done by playing a movie, or by presenting text with accompanying pictures. User input is usually limited to a few choices like "skip" or "continue".

Dialog

dialog example A Dialog is one of the more tricky things to do in a game. It is usually a rectangle that pops up over the Main GUI containing buttons, text, or other widgets (like an RPG "inventory" dialog). While the Dialog is up, the presentation of the Main GUI is usually not interrupted (though it could be). Things still may move around in the background, but the Dialog is understood to have focus. For instance, if a "chat" dialog is present, the keypresses that usually make a Charactor move (ie "WASD") will now go only to the "chat" dialog so the user can type in a message. If a "yes/no" dialog has popped up such that the "no" button is over a charactor on the screen, and the user clicks the "no" button, that click should not select the charactor underneath, it should only press the "no" button.

FAQ