forked from sjbrown/writing_games_tutorial
-
Notifications
You must be signed in to change notification settings - Fork 0
/
writing-games.html
2244 lines (1990 loc) · 95.6 KB
/
writing-games.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>sjbrown's Guide To Writing Games</title>
<style type="text/css">
body {
font-family: sans-serif;
font-family: "Bitstream Charter";
margin-top: 5px;
margin-bottom: 3em;
margin-left: 3em;
}
body > h1 {
margin-top: 2em;
margin-left: -1em;
}
code {
margin-left: 1em;
margin-right: 1em;
}
pre.python { text-align: left; padding: 0px}
pre.python b { color: blue; font-weight: normal; }
.diversion {
margin: 2em;
padding-left: 1em;
border-left: solid black thin;
}
.codeblock {
background-color: #F0F0FF;
padding: 1em;
width: 70%;
margin: 0.8em;
border: dashed 1px #D0D0FF;
}
.snippet {
background-color: #E9E9F0;
padding: 0.5em;
width: 70%;
margin: 0.5em
}
.console {
border: solid thin #aaaaaa;
margin: 1ex;
padding: 5px;
text-align: center;
float: left;
}
.clear {
clear: both;
}
pre.bash {
padding: 8px;
text-align: left;
padding: 0px;
color: white;
background-color: black;
width: 80ex;
}
.warning {
border-top: solid 4px #FF0000;
background-color: #FFFAFA;
margin-top: 30px;
margin-left: -10px;
padding: 10px;
}
.sourceLinks {
border: solid thin #0000FF;
margin: 1em;
padding: 1em;
}
.todo {
color: #AA6699;
}
img.figure {
vertical-align: middle;
margin-top: 1em;
margin-bottom: 1em;
}
img.internal {
float: left;
vertical-align: middle;
margin-right: 0.5em;
margin-bottom: 1em;
}
h3 { clear: both; }
li.q { font-weight: bold; list-style-type: disc;}
li.a { padding-bottom: 1em; list-style-type: circle;}
</style>
</head>
<body>
<!--
<b style="color:red">NOTE: this tutorial is currently "in-progress". It's not
done, but it covers a lot of ground and is "useful" in it's current state.
Also, it is currently out-of-sync with the latest release of Twisted, so
the later code will probably barf due to API changes if run against the
current version of Twisted.</b>
-->
<h1>sjbrown's Writing Games Tutorial</h1>
<p>By Shandy Brown. Please send comments / corrections via email to <b>[email protected]</b></p>
<p><b style="color:blue">Last Update:</b> March 2011
</p>
<hr>
<a href="table.html">Table Of Contents</a>
<hr>
<!--
<h1><a name="purpose">Purpose</a></h1>
<span class="todo">[TODO]</span>
<h1><a name="twitch">Twitch vs. Non-Twitch</a></h1>
First of all, let me say what this guide is <b>not</b>. Some games you may be
used to playing are known as "Twitch" games. In these games the speed by which
you press the buttons or move your mouse have an effect on the outcome of the
game. In "Street Fighter", if you press your punch button a couple miliseconds
before your opponent, it can matter.
<p>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 applicable to games where it is acceptable to have
a slight (if imperceptable) lag between user input and screen response.</p>
<p>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.</p>
-->
<h1><a name="know">What You Should Know</a></h1>
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.
<h2><a name="oop">Object Oriented Programming</a></h2>
It is expected the reader is comfortable in an object oriented environment.
All the important code is structured with classes.
<h2><a name="patterns">Design Patterns</a></h2>
Design Patterns are a communication tool; they do not dictate design, they
inform the reading of the code.
This guide makes use of the Design Patterns "Model View Controller" (MVC),
"Mediator", and "Lazy Proxy".
Time won't be spent describing these patterns in detail, so if they sound
foreign to you, I recommend checking out the book
"Design Patterns" by Gamma et al. or just surfing the web for tutorials.
<h1><a name="part1">PART 1</a></h1>
<h1><a name="example">Example Goal</a></h1>
It's always a good idea to sketch out your game either with pictures or text
before you begin coding.
<p>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 extensible
so we won't get tied up in the game rules, instead we can focus on the
structure of the code.</p>
<center><img src="mockup.png" alt="example applicaton" width="400" height=
"400"></center>
<h1><a name="architecture">The Architecture</a></h1>
<h2><a name="mvc">Model View Controller</a></h2>
The choice of MVC should be pretty obvious where a graphical game is
concerned. The primary Model will be discussed later under the heading
<a href="#hModel">The Game Model</a>. 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.
<p>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:</p>
<div class="codeblock">
<pre class="python">
<b>#<i>stolen from the ChimpLineByLine example at pygame.org</i></b>
main():
...
while 1:
<b>#Handle Input Events</b>
for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == MOUSEBUTTONDOWN:
fist.punch()
elif event.type == MOUSEBUTTONUP:
fist.unpunch()
<b>#Draw Everything</b>
allsprites.update()
screen.blit(background, (0, 0))
allsprites.draw(screen)
pygame.display.flip()
</pre></div>
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 games work, 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.
Experience tells us that as the code grows, this section will get hairy.
Organizing this in an MVC pattern, we separate the View and the Controller.
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:
<div class="codeblock">
<pre class="python">
ControllerTick():
<b>#Handle Input Events</b>
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():
<b>#Draw Everything</b>
...
main():
...
while 1:
if not ControllerTick():
return
ViewTick()
</pre></div>
Here is some more info on the MVC pattern:
<a href=
"http://en.wikipedia.org/wiki/Model-view-controller">MVC @ Wikipedia</a>
<a href=
"http://ootips.org/mvc-pattern.html">MVC @ ootips.org</a>
<div class="diversion">
<h3><a name="caveat">Rationale</a></h3>
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, HUDs,
multiple zoom levels, ...), keep the Model code "cleaner" by decoupling
it from the view and the controller, and I contend, more readable code.
</div>
<h2><a name="mediator">Mediator</a></h2>
<p>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.
<p>Perhaps if "wall clock" 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.</p>
<center><img src="arch.png" class="figure"
alt="architecture" width="300" height="280"></center>
<p>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.</p>
<p>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.
<span class="todo">[TODO: more weakref rationale. gc, etc]</span></p>
<p>We will also create an Event class to encapsulate the events that can be sent
via the EventManager.</p>
<div class="codeblock">
<pre class="python">
class Event:
<b>"""this is a superclass for any events that might be generated by an
object and sent to the EventManager
"""</b>
def __init__(self):
self.name = "Generic Event"
class EventManager:
<b>"""this object is responsible for coordinating most communication
between the Model, View, and Controller.
"""</b>
def __init__(self ):
from weakref import WeakKeyDictionary
self.listeners = WeakKeyDictionary()
<b>#----------------------------------------------------------------------</b>
def RegisterListener( self, listener ):
self.listeners[ listener ] = 1
<b>#----------------------------------------------------------------------</b>
def UnregisterListener( self, listener ):
if listener in self.listeners.keys():
del self.listeners[ listener ]
<b>#----------------------------------------------------------------------</b>
def Post( self, event ):
<b>"""Post a new event. It will be broadcast to all listeners"""</b>
for listener in self.listeners.keys():
<b>#NOTE: If the weakref has died, it will be </b>
<b>#automatically removed, so we don't have </b>
<b>#to worry about it.</b>
listener.Notify( event )
</pre></div>
Here is a rough idea how this might be integrated with the previous code.
<div class="codeblock">
<pre class="python">
class KeyboardController:
...
def Notify(self, event):
if isinstance( event, TickEvent ):
<b>#Handle Input Events</b>
...
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 ):
<b>#Draw Everything</b>
...
main():
...
evManager = EventManager()
keybd = KeyboardController()
spinner = CPUSpinnerController()
pygameView = PygameView()
evManager.RegisterListener( keybd )
evManager.RegisterListener( spinner )
evManager.RegisterListener( pygameView )
spinner.Run()
</pre></div>
<div class="diversion">
<h3><a name="selective">Diversion: Event Types and Selective
Listeners</a></h3>
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.
<p>For the purpose of this guide, we'll just use one kind of event, so every
listener gets spammed with every event.</p>
</div>
<div class="diversion">
<h3><a name="evmanager">Advanced Event Managers</a></h3>
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 the later
examples, you can see an example of a more advanced Event Manager that
always delivers events in the desired order.
</div>
Here is some more info on the Mediator pattern and the related Observer
pattern:
<a href="http://en.wikipedia.org/wiki/Mediator_pattern">Mediator @ Wikipedia</a>
<a href="http://ootips.org/observer-pattern.html">Observer @ ootips.org</a>
<a name="hModel"></a>
<h1><a name="hModel">The Game Model</a></h1>
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.
<p>
Here is a Model that has worked for me and is general enough to adapt to many types of games:
<center><img src="game-model.png" class="figure" alt="example applicaton" width="269" height=
"206"></center>
<h2><a name="game">Game</a></h2>
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.
<h2><a name="player">Player</a></h2>
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.
<h2><a name="charactor">Charactor</a></h2>
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.
<p>In our example, "little man" will be our sole Charactor.</p>
<h2><a name="map">Map</a></h2>
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.
<p>In our example, the Map will be a discrete Map having a simple list of nine
sectors.</p>
<h2><a name="sector">Sector</a></h2>
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
<i>in between</i>
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 <i>look</i> like
it is in between Sectors, but that is an issue for the View, not the Model)
<p>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.</p>
<h2><a name="location">Location</a></h2>
We won't get into Locations of a continuous Map, as they don't apply to our
example.
<h2><a name="item">Item</a></h2>
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 <b>must</b>
be contained by Charactors (perhaps in an intermediate "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.
<h1><a name="our">Our Example</a></h1>
<div class="sourceLinks">The code can be downloaded here:
<a href="code_examples/example.py">example.py</a><br>
Or read it in your browser
<a
href="http://github.com/sjbrown/writing_games_tutorial/blob/example1/code_examples/example.py">here</a>
</div>
<center><img src="screenshot-example1.png" alt="example applicaton" width=
"434" height="468"></center>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>Finally we have the Model objects as discussed above and ultimately the
main() function.</p>
<p>Here is a diagram of the major incoming and outgoing events.</p>
<center><img src="diagram-incoming.png" class="figure"
alt="example incoming messages" width="400" height="340">
<img src="diagram-outgoing.png" class="figure"
alt="example outgoing messages" width="400" height="328"></center>
<h1><a name="part2">PART 2</a></h1>
<h1><a name="internet">Internet Play</a></h1>
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.
<p>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.
<div class="diversion">
<h3><a name="rapid">Rapid Development</a></h3>
One of the goals of this tutorial is to show how game development can be done
<i>rapidly</i>. Usually anything involving networking is anathema to "rapid"
because once you introduce networking, you introduce multiprocessing, latency,
error handling, and general hair-pulling. A principle of the code examples
in this tutorial is to make sure that the game can be run without even turning
on networking. The networking feature should have zero impact on the code
in example.py -- running the game in single-player mode should not execute any
networking-related code paths. By being strict about this separation, we hope
to make it possible to develop the <i>game</i> rapidly, no matter what snags
the networking code may introduce.
</div>
<center><img src="host_structure.png" class="figure"
alt="the different ways to structure network hosts"
width="800" height="212"></center>
<h3><a name="clientserver">Structure of Network Hosts</a></h3>
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 competitive, 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.
<p>
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.
<p>
<a href="http://en.wikipedia.org/wiki/Client_server">Client-Server @ Wikipedia</a>
<p>
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.
<p>
<a href="http://en.wikipedia.org/wiki/Servent">Servent @ Wikipedia</a>
<p>
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.
<p>
<a href="http://en.wikipedia.org/wiki/Peer-to-peer">Peer to Peer @ Wikipedia</a>
<p>
For our examples, we will examine the "Strict" Client-Server structure.
<h2><a name="sync">Synchronous / Asynchronous</a></h2>
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.
<p>
This asynchronous 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 asynchronous messages
from the network fairly painless.
<p>
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.
<p>
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.
<h2><a name="implementation">Implementation</a></h2>
<h3><a name="exServer">Example Server</a></h3>
For the server, we'll start with the exact same code as before. Just rename
example.py to server.py.
<p>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:</p>
<div class="codeblock">
<pre class="python">
<b>#------------------------------------------------------------------------------</b>
class TextLogView:
"""..."""
def __init__(self, evManager):
self.evManager = evManager
self.evManager.RegisterListener( self )
<b>#----------------------------------------------------------------------</b>
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
</pre></div>
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.
<p>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.</p>
<div class="codeblock">
<pre class="python">
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
</pre></div>
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.
<div class="diversion">
<h3><a name="caveat">Caveat</a></h3>
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.
<p>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.
<span class="todo">[TODO: expand on this]</span></p>
</div>
<p>
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.
<div class="codeblock">
<pre class="python">
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()
</pre></div>
<p>Previously, we used the Tick event to start the Game, now we'll
need to explicitly start the game with our new GameStartRequest event.</p>
<div class="codeblock">
<pre class="python">
class GameStartRequest(Event):
def __init__(self):
self.name = "Game Start Request"
</pre></div>
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.
<p>
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.
<div class="console">
<pre class="bash">
$ 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()
</pre>
Example of using the Python console as a fake client</div>
<div class="console">
<pre class="bash">
$ python server.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>
</pre>
Running server.py
<p>
Note that the request to move up did not result in a Move event.
</div>
<p class="clear"> </p>
<p>
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:
<div class="console">
<pre class="bash">
$ 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
</pre>
Using twisted.conch.stdio as a fake client</div>
<p class="clear"> </p>
<div class="sourceLinks">The code for server.py can be downloaded here:
<a href="code_examples/server.py">server.py</a><br>
Or read it in your browser
<a href="http://github.com/sjbrown/writing_games_tutorial/blob/example1/code_examples/server.py">here</a>
</div>
<h2><a name="king"></a>King of the Castle</h2>
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.
<div class="codeblock">
<pre class="python">
<b># Example of a class that pumps a Twisted reactor</b>
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() <b>#excrete anything left in the reactor</b>
def onReactorStop(self):
<b>'''This gets called when the reactor is absolutely finished'''</b>
self.reactor = None
</pre></div>
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.
<div class="codeblock">
<pre class="python">
<b># Example of using LoopingCall to fire the Tick event</b>
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)
</pre></div>
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.
<h2><a name="wire">Messages Over the Wire</a></h2>
<div class="sourceLinks">The source code to the remainder of this section is
in multiple files. You can download them in tar.gz format here:
<a href="examples/example2.tar.gz">example2.tar.gz</a>, or browse
the source code
<a href="http://github.com/sjbrown/writing_games_tutorial/tree/example2/code_examples">here</a>.
</div>
<p>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.</p>
<p>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:</p>
<ol>
<li>Do we need to send it from the client to the server?</li>
<li>Do we need to send it from the server to the client?</li>
<li>Are there security issues with sending this data over the network?</li>
<li>Is the data formatted in a way that it can be sent over the network?</li>
<li>If we must, how do we reformat the data so that it can be sent?</li>
</ol>
(Eventually, one might also ask "How often will this message be sent?" and
therefore "How can I best optimize this message?")
<p>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.</p>
<p>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 <span class="todo">[TODO: ask someone who knows Twisted if
that's <b>really</b> necessary].</span> Things
can become even <i>more</i> 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 <b>doesn't</b> require special formatting
are the
<a href="http://www.python.org/doc/current/ref/literals.html">literal</a>
types: string, int, float, etc., None, and containers (lists, tuples, dicts)
thereof.</p>
<p>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. <span class="todo">[TODO: link to explain Mixins
somewhere]</span></p>
<p>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.</p>
<div class="codeblock">
<pre class="python">
<b># from network.py</b>
<b>#------------------------------------------------------------------------------</b>
<b># GameStartRequest</b>
<b># Direction: Client to Server only</b>
MixInCopyClasses( GameStartRequest )
pb.setUnjellyableForClass(GameStartRequest, GameStartRequest)
clientToServerEvents.append( GameStartRequest )
<b>#------------------------------------------------------------------------------</b>
<b># CharactorMoveRequest</b>
<b># Direction: Client to Server only</b>
<b># this has an additional attribute, direction. it is an int, so it's safe</b>
MixInCopyClasses( CharactorMoveRequest )
pb.setUnjellyableForClass(CharactorMoveRequest, CharactorMoveRequest)
clientToServerEvents.append( CharactorMoveRequest )
</pre></div>
<p>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 receive an event from the network
referencing an object by its ID number, we can find the actual object.</p>
<div class="codeblock">
<pre class="python">
<b># from network.py</b>
<b>#------------------------------------------------------------------------------</b>
<b># GameStartedEvent</b>
<b># Direction: Server to Client only</b>
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 )
<b>#------------------------------------------------------------------------------</b>
<b># CharactorMoveEvent</b>
<b># Direction: Server to Client only</b>
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 )
</pre></div>
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.
<h3><a name="channel">Creating A Communication Channel</a></h3>
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)
<p>From the server, changes need to be sent out, so we need to create a new
View on the server.</p>
<div class="codeblock">
<pre class="python">
<b># from server.py</b>
<b>#------------------------------------------------------------------------------</b>
class NetworkClientView(object):
<b>"""We SEND events to the CLIENT through this object"""</b>
def __init__(self, evManager, sharedObjectRegistry):
self.evManager = evManager
self.evManager.RegisterListener( self )
self.clients = []
self.sharedObjs = sharedObjectRegistry