Skip to main content

tut_design — wiki

Back to Tutorials

What You Should Know

This guide assumes a certain level of knowledge. If it is confusing, perhaps you should brush up on some of these concepts.

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

Here is some more info on the MVC pattern: http://ootips.org/mvc-pattern.html

Mediator

Let's examine the infinite while loop in the last bit of code. What is it's 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 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 )

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

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.

Here is some more info on the Mediator pattern: http://ootips.org/observer-pattern.html

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.

The Game Model

Here's the basic Model:

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. The screen in Scorched Earth, or a level in Super Mario 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 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 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

... More of this tutorial can be found at http://sjbrown.ezide.com/games/writing-games.html