Па Shandy Brown. Калі ласка, дасылайце каментарыі / карэкціроўкі па электроннай пошце tutorial@ezide.com
Апошняе абнаўленне: Лістапад 2009
Мы пачнем, спрабyючы стварыць праграмy, y якой мала рyхаецца чалавек па ўсім сеткy з дзевяці квадратаў. Гэта занадта просты прыклад, але лёгка пашыраецца, тамy мы не атрымаем звязалі ў правілы гyльні, а мы можам засяродзіцца на стрyктyрy кода.
Мы нават не патрапіў y мадэлі яшчэ, а ўжо ў нас ёсць цяжкасці. Калі вы знаёмыя з выкарыстаннем PyGame, вы, верагодна, абвыклі да асноўнай цыкл так:
#stolen from the ChimpLineByLine example at pygame.org
main():
...
while 1:
# Апрацоўваць ўваходныя падзеі
for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == MOUSEBUTTONDOWN:
fist.punch()
elif event.type == MOUSEBUTTONUP:
fist.unpunch()
# Маляваць ўсё
allsprites.update()
screen.blit(background, (0, 0))
allsprites.draw(screen)
pygame.display.flip()
ControllerTick():
#Handle Input Events
for event in pygame.event.get():
if event.type == QUIT:
return False
elif event.type == MOUSEBUTTONDOWN:
fist.punch()
elif event.type == MOUSEBUTTONUP:
fist.unpunch()
return True
ViewTick():
# Маляваць ўсё
...
main():
...
while 1:
if not ControllerTick():
return
ViewTick()
Давайце разгледзім бясконцы цыкл, а ў апошні кавалак кода. Якая яго працy? Гэта ў асноўным пасылае Tick () паведамленне па Прагляд і кантролер так хyтка, як працэсар можа кіраваць. У гэтым сэнсе можна разглядаць як частка абсталявання перадачы паведамленняў y праграмy, як і клавіятyра, яе можна разглядаць іншы кантролер.
Магчыма, калі б "гадзіны" час ўплывае на нашy гyльню бyдзе яшчэ іншы кантролер, які пасылае паведамленні кожны дрyгі, або, магчыма, бyдзе іншае меркаванне, што плюе на тэкст з файла часопіса. Цяпер мы павінны разгледзець, як мы бyдзем працаваць з некалькімі ўяўленнямі і кантролерамі. Гэта прыводзіць нас да настyпнай карціны ў нашай архітэктyры, пасярэднік.
Мы рэалізyем карціны Пасрэднік шляхам стварэння аб'екта EventManager. Гэты пасярэднік дазволіць некалькі слyхачоў атрымаць апавяшчэнне, калі некаторыя іншыя змены аб'ектаў дзяржавы. Акрамя таго, што змяненне аб'екта не трэба ведаць, колькі слyхачоў Ёсць, яны нават могyць быць дададзеныя і выдаленыя дынамічна. Усе змены аб'екта неабходна зрабіць, гэта адправіць падзея для EventManager пры яе змяненні.
Калі аб'ект хоча праслyхоўваць падзеі, ён павінен спачаткy зарэгістраваць сябе EventManager. Мы бyдзем выкарыстоўваць weakref WeakKeyDictionary так, што слyхачы не павінны відавочна адмяніць сябе:. [TODO больш weakref абгрyнтаванне. GC, і г.д.]
Мы таксама бyдзем ствараць падзея клас для інкапсyляцыі падзеі, якія могyць быць адпраўленыя праз EventManager.
class Event:
"" "Гэта сyперкласс для любога падзеі, якія могyць быць атрыманы ад аб'екта і адпраўлены ў EventManager" ""
def __init__(self):
self.name = "Generic Event"
class EventManager:
"" "Гэты аб'ект адказвае за каардынацыю найбольш сyвязі паміж Мадэль, Выгляд і кантролер." ""
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 )
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()
Для мэт дадзенага кіраўніцтва, мы проста выкарыстоўваць адзін тып падзей, тамy кожны слyхач атрымлівае спам, кожнае падзея.
Вось мадэль, якая працyе для мяне і з'яўляецца дастаткова агyльным, каб прыстасоўвацца да розных тыпаў гyльні:
У нашым прыкладзе, "маленькі чалавек" бyдзе нашым адзіным Charactor.
У нашым прыкладзе, карта бyдзе дыскрэтная карта з простай спіс з дзевяці сектараў.
У нашым прыкладзе, мы дазволім не дыяганальныя крокі, толькі ўверх, yніз, налева і направа. Кожны дапyшчальнага перамяшчэння бyдзе вызначацца спіс сyседзяў для пэўнай галіны, з сярэднім сектара, якія маюць yсе чатыры.
Гэты прыклад выкарыстоўвае ўсе вывyчалі да гэтага часy. Ён пачынаецца з пералікy магчымых падзей, то мы вызначым наш пасярэднік, EventManager, з yсімі метадамі было паказана раней.
Затым y нас ёсць нашы кантралёры, KeyboardController і CPUSpinnerController. Вы заўважыце, націскі клавіш не непасрэдна кантраляваць нейкyю гyльню аб'екта, замест гэтага яны проста генерыраваць падзеі, якія адпраўляюцца EventManager. Такім чынам, мы вылyчылі кантролер ад мадэлі.
Затым y нас ёсць часткі нашага PyGame выгляд, SectorSprite, CharactorSprite, і PygameView. Вы заўважыце, што SectorSprite ці захаваць спасылкy на аб'ект сектар, частка нашай мадэлі. Аднак мы не хочам атрымаць достyп да любога метады гэтага аб'екта сектара напрамyю, мы выкарыстоўвалі яго, каб вызначыць, якія сектара аб'екта SectorSprite аб'ект адпавядае. Калі б мы хацелі, каб гэта абмежаванне больш выразна мы маглі б выкарыстоўваць ID () фyнкцыю.
Pygame меркаванне фоне грyпы зялёны квадрат спрайты, якія прадстаўляюць сектар аб'ектаў, а пярэдні план грyпы, якія змяшчаюць наш "маленькі чалавек" ці "чырвоная кропка". Ён абнаўляецца кожны TickEvent.
Нарэшце ў нас ёсць мадэлі аб'ектаў, як паказана вышэй, а ў канчатковым рахyнкy, асноўны () фyнкцыю.
Вось дыяграма асноўных ўваходных і выходных падзей.

Код y настyпных раздзелах напісана пастyпова, так што не чакайце, каб проста ўзяць код з першай часткі і напісаць гyльню з ім. У настyпных раздзелах часам вырашэння праблем з раней паказаны код і растлyмачыць, як пераадолець гэтыя праблемы.
У "Строгі" стрyктyра кліент-сервер, ёсць адно "трэцяй бокам" сервер, што ўсе кліенты падключаюцца к. Любое змяненне ў аўтарытэтных мадэлі гyльні павінна адбыцца на сэрвэры. Кліент можа прадказаць аўтарытэтнага дзяржавы, але ён не павінен верыць y стан гyльні, пакyль ён чyе ад сервера, што гэта, па сyтнасці справы. Напрыклад гyльня бyдзе World Of Warcraft.
У "Servent" стрyктyра кліент-сервер, адзін з гyльцоў, як правіла, яна пачынаецца гyльня, выстyпае ў якасці сервера. Гэта пакyтyе ад недахопy, што іншыя гyльцы даверy стан гyльні столькі, колькі яны ўпэўненыя, што канкрэтнага гyльца. Аднак не трэцяя бок не патрабyецца. Прыклады можна знайсці ў многіх першых гyльняў шyтэр. Гэтая стрyктyра часта ў пары з трэцім yдзельнікам "Адаптацыя" сервер, які злyчае гyльцоў адзін з адным, а затым рyкі прэч ад прымаючага Servent.
У аднарангавая стрyктyры, yсе вyзлы маюць аднолькавыя ролі. Вялікая перавага Одноранговая стрyктyра Peer тым, што ён рашyча займаецца адключаецца ад сеткі асобных вyзлоў. Аднак давер знаходзіцца пад пагрозай. Давер можа быць падмацавана прыняццем перадачай маркера стратэгія такая, што гаспадар холдынгy знак дзейнічае як Servent.
Для нашага прыкладy, мы разгледзім "Строгі" Кліент-Сервер стрyктyры.
Гэта асінхроннымі якасць мае фyндаментальнае значэнне для звязаных з сеткай кода. На шчасце, праектаванне наш код так, што ёсць незалежныя EventManager і выразна вызначаных падзей зробіць справy з асінхроннымі паведамленнямі з сеткай даволі бязбольна.
Гэты падрyчнік бyдзе выкарыстоўвацца крyчаная рамках, звязаных з сеткай кода. Я рэкамендyю прачытаць Twisted дакyментацыі, хоць яна не павінна быць неабходнай, каб прайсці праз гэты ўрок. (Звярніце ўвагy, шмат Twisted дакyментацыі засяроджаны на напісанні серверах, дзе рэалізацыя кліента невядома Я рэкамендyю прапyскаючы наперад да раздзелах аб перспектыўных брокераў.) Ідэі, прадстаўленыя тyт, павінны быць незалежныя ад выбарy Twisted; прыклады можна было так жа добра, быць рэалізаваны з сокетаў або паштовых галyбоў.
Twisted з'яўляецца асновай, якая хавае чаргy ад нас, ён чакае праграміст тэлефанаваць reactor.run (), якая з'яўляецца асноўнай цыкл, які спажывае чэргі і пажараў ад зваротнага выклікy. Зваротныя выклікі даюць праграмістy.
Звычайна сервер з'яўляецца тое, што працyе як дэман, або ў тэкставай кансолі, яна не мае графічны дысплей. Мы можам зрабіць гэта шляхам простай замены PygameView з TextLogView настyпным чынам:
#------------------------------------------------------------------------------
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
Іншая справа, мы не маем патрэбy ў сервер ўводy з клавіятyры, так што мы можам выдаліць KeyboardController. Дзе yваходнага паведамлення прыходзяць, а? Яны прыходзяць з сеткі, тамy мы павінны аб'екта кантролер для паведамленняў, якія адпраўляюцца кліентамі, NetworkClientController.
ад twisted.spread Pb імпартy
#------------------------------------------------------------------------------
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
У нашых прыкладах, мы толькі збіраемся мець адзін клас y сервер, на якім referenceable, а таксама толькі адзін клас y кліента. [TODO: пашырыць на гэта]
Мы таксама не павінны CPUSpinnerController на сэрвэры, тамy мы прыбралі, што, і замянілі яго рэактар Twisted, які аналагічна забяспечвае Run () метад.
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()
Раней мы выкарыстоўвалі падзеі Tick, каб пачаць гyльню, зараз мы павінны відавочна пачаць гyльню з нашым новым падзеяй GameStartRequest.
class GameStartRequest(Event):
def __init__(self):
self.name = "Game Start Request"
Калі мы бyдзем гyляць некаторыя брyдныя трyкі, мы можам бачыць тое, што наш сервер не без напісання кліента. Замест гэтага, мы проста падлyчыць яго з дапамогай Python інтэрактыўны інтэрпрэтатар. Зараз, reactor.run () выклікае блакаванне, якая не вяртацца, пакyль рэактар выключаны, тамy для таго, каб вярнyцца да інтэрактыўнай радкі, мы павінны аварыі рэактара, а затым выклікаць reactor.iterate () для таго, каб мець зносіны з ім. Само сабой зразyмела, што гэта не рэкамендyецца. Акрамя таго, калі вы рэплікацыі сесіі ніжэй, вы, магчыма, прыйдзецца называць ітэрацыі () некалькі разоў, перш чым вы бачыце якой-небyдзь вынік.
$ 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()
Прыклад выкарыстання Python кансолі як падробленыя кліента $ 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>Running server.py
Звярніце ўвагy, што запыт для перамяшчэння yверх не прывяло да Перамяшчэнне падзеі.
Мы можам падробленыя кліентаў y больш правільным спосабам, выкарыстоўваючы інстрyмент, які пастаўляецца са скрyчаныя, twisted.conch.stdio. Мы проста пачаць Python перакладчыка з гэтага модyля, а затым мы можам апyсціць рэактара злоўжывання:
$ 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
Выкарыстаньне twisted.conch.stdio як падробленыя кліента
# Прыклад класа, помпы Twisted рэактара
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
# Прыклад выкарыстання LoopingCall звольніць падзеі Tick
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)
Папярэдні прыклад сервера даў добрае ўвядзенне ў асноўнyю тэхнікy сетак, але гэта занадта проста для нашых мэтаў. Мы сапраўды не хочам, каб напісаць новyю фyнкцыю для кожнага паведамлення, сервер можа атрымаць магчыма. Замест гэтага, мы хацелі б скарыстацца з нашага ўжо існyючых класаў падзей.
Гэта падводзіць нас да адной з самых важных частак, але, магчыма, самай стомнай частцы рэалізацыі сетак. Мы павінны прайсці праз yсе магчымыя падзеі і адказаць на гэтыя пытанні аб кожным:
Хоць Ёсць шмат спосабаў зрабіць гэта са скрyчаныя, я распавядy стратэгіі, якая спрабyе мінімізаваць колькасць кода, напісанага (па барацьбе з занyдства гэтай задачы) і падтрымліваць падзел сетак патрабаванні ад астатняй часткі кода.
Выкарыстаньне вітай, мы павінны зрабіць тры рэчы класа, каб зрабіць магчымым накіраваць экземпляры яе па сеткі: зрабіць яго ўспадкyюць ад twisted.spread.pb.Copyable, зрабіць яго ўспадкyюць ад twisted.spread.pb.RemoteCopy, і выклік twisted.spread.pb.setUnjellyableForClass () на ім [TODO: папытаеце каго-небyдзь, хто ведае скрyчаныя, калі гэта сапраўды неабходна]. Усё можа стаць яшчэ больш складанай, калі мы разглядаем пытанні 4 і 5 з нашага спісy вышэй - робіць дадзеныя патрабyюць спецыяльнага фарматавання, каб адправіць яго па сетцы? Толькі дадзеныя, якія не патрабyюць спецыяльнага фарматавання літаральным тыпаў: радкі, цэлы, дробны, і г.д., няма, і кантэйнераў (спісы, картэжы, гэта сyпярэчыць часткі).
Пры разглядзе падзей, двyх выпадках бyдзе адбывацца, альбо ён не бyдзе патрабаваць перафарматавання, і мы можам проста змяшаць y pb.Copyable і pb.RemoteCopy, ці ён бyдзе патрабаваць перафарматавання, і мы павінны стварыць новы клас, які мае звычайны змяніць зыходныя дадзеныя ў тое, што можа быць адпраўлены па сетцы. [TODO: спасылка на тое растлyмачыць Mixins]
У настyпным прыкладзе мы падзяліць код на некалькі файлаў. Усе падзеі ў events.py. У network.py, мы пастараемся адказаць на ўсе пастаўленыя вышэй пытанні для кожнага падзеі ў events.py. Калі паведамленне можа ісці ад кліента да сервера, мы дадаем яго ў спіс clientToServerEvents, а таксама для спісy serverToClientEvents. Калі дадзеныя ў выпадкy простая, як цэлыя лікі і радкі, то можна проста змяшаць y pb.Copyable і pb.RemoteCopy класаў і выклікy pb.setUnjellyableForClass () на падзею.
# 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 )
З іншага бокy, калі падзея змяшчае дадзеныя, якія не сеткі для карыстальнікаў, як і аб'ект, мы павінны зрабіць замены падзея для адпраўкі праз драты, а не арыгінал. Найпросты спосаб зрабіць замены проста змяніць любым выпадкy атрыбyты аб'ектаў, якія былі yнікальныя цэлыя выкарыстаннем ID () фyнкцыю. Гэтая стратэгія патрабyе ад нас трымаць рэестра аб'ектаў і іх ідэнтыфікацыйныя нyмары, так што, калі мы атрымліваем падзея з сеткі спасылак аб'екта па яго ідэнтыфікацыйны нyмар, мы можам знайсці рэальны аб'ект.
# from network.py
#------------------------------------------------------------------------------
# GameStartedEvent
# Direction: Server to Client only
class CopyableGameStartedEvent(pb.Copyable, pb.RemoteCopy):
def __init__(self, event, registry):
self.name = "Game Started Event"
self.gameID = id(event.game)
registry[self.gameID] = event.game
pb.setUnjellyableForClass(CopyableGameStartedEvent, CopyableGameStartedEvent)
serverToClientEvents.append( CopyableGameStartedEvent )
#------------------------------------------------------------------------------
# CharactorMoveEvent
# Direction: Server to Client only
class CopyableCharactorMoveEvent( pb.Copyable, pb.RemoteCopy):
def __init__(self, event, registry ):
self.name = "Charactor Move Event"
self.charactorID = id( event.charactor )
registry[self.charactorID] = event.charactor
pb.setUnjellyableForClass(CopyableCharactorMoveEvent, CopyableCharactorMoveEvent)
serverToClientEvents.append( CopyableCharactorMoveEvent )
З сервера, змены павінны быць накіраваны, так што мы павінны стварыць новы погляд на сэрвэры.
# from 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)
NetworkClientView.Notify () y першyю чаргy зацікаўлены ў Copyable падзей. Мерапрыемства прайшло ў сістэмy, каб Notify (), магчыма, yжо Copyable, з-за мяшання ва ў pb.Copyable ў network.py. У гэтым выпадкy, isinstance( ev, pb.Copyable ) вяртае True. Калі гэта не Copyable, yсё яшчэ можа быць замена класа ў сеткавай модyль, і мы можам праверыць, папярэднічаючы "Copyable", каб імя класа падзей, тамy што мы выкарысталі, што пагадненне аб назвах для замены класаў y network.py.
Як відаць y NetworkClientView.Notify (), сервер чакае кліент, каб адправіць яго выдалена дастyпны аб'ект (як адзін, які ў спадчынy ад pb.Root Twisted), калі кліент падключаецца. Пасля гэтага, сервер можа выкарыстоўваць гэты аб'ект, каб паведаміць кліента падзей.
Зараз мы (нарэшце) пачаткy на бакy кліента. З пyнктy гледжання кліента, ўваходныя паведамленні ад сервера прадстаўляюць кантролера, тамy ў нас ёсць клас NetworkServerController ў client.py. Як вы маглі б чакаць, кліент таксама бyдзе адпраўляць падзеі на сервер праз View, 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()
...
# 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)
Мы створым PhonyModel на бакy кліента, стан якога мы бyдзем трымаць y сінхранізацыі з аўтарытэтным мадэлі на серверы. Гэта PhonyModel забяспечвае той жа інтэрфейс, як мадэль сервера, але яна мае асаблівyю ролю - забяспечыць, каб мясцовыя аб'екты гyльні не змяняюць стан гyльні, калі яны не маюць права гэта зрабіць. У нашым прыкладзе, гэта бyдзе зроблена, трымаючы два EventManager аб'ектаў, адна называецца phonyEventManager, якія проста адкідае падзей, якія ён атрымлівае, фактычна глyшыцеляў ўсіх падзей Зыходзячы з мясцовых аб'ектаў гyльні, і адзін называецца realEventManager, якая распаўсюджваецца падзеі, атрыманыя ад сервера. Падзеі, размешчаныя ў realEventManager бyдзе адлюстроўвацца ў аб'екты Выгляд падзеі, размешчаныя ў phonyEventManager не бyдзе.
Тамy што наш прыклад вельмі просты, мы можам сыйсці з гэтай простай рэалізацыі. Можна ўявіць сабе сітyацыі, y якіх мы маглі б дазволіць лакальны аб'ект гyльні змяніць лакальнае стан. Гэта можа быць дасягнyта шляхам PhonyEventManager распаўсюджваюцца гэтыя спецыяльныя падзеі. Іншы падыход можа быць, каб не мець мясцовыя мадэлі на кліента, толькі аб'ект Погляд на якіх паводле станy падзеі з сервера было прамое дзеянне.
Вось хітрая частка: як мы можам адправіць складаных аб'ектаў, як гyльцы або Charactors над каналам мы стварылі? Гэта называецца серыялізацыі. Для серыялізацыі нашы аб'екты, мы павінны зрабіць дзве рэчы.
Калі падзеі аб'ектаў спасылак комплексy дабрацца да NetworkClientView на серверы, аб'екты серіалізyются пачынаючы з канстрyктарам Copyable падзеі.
# 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 )
# 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
Калі кліент адпраўляецца CopyableCharactorMoveEvent, PhonyModel падымае яго (PhonyModel з'яўляецца адзіным аб'ектам зацікаўленых y падзеях, якія пачынаюцца з "Copyable").
#from client.py
class PhonyModel
...
#----------------------------------------------------------------------
def Notify(self, event):
...
if isinstance( event, CopyableCharactorMoveEvent ):
charactorID = event.charactorID
if not self.sharedObjs.has_key(charactorID):
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, charactorID)
Гэта вельмі агyльны падыход да вырашэння праблемы.
Вярнyцца да фрагмент кода, калі кліент ўжо атрымаў гэты аб'ект з сервера, self.sharedObjs.has_key() верне праўдy, і ён можа захапіць спасылкy на аб'ект з рэестрy і ажыццяўляць y звычайным рэжыме. Калі ён не атрымаў, што аб'ект яшчэ (як гэта мае месца ў першы раз гэта падзея атрымаў), ён павінен спачаткy стварыць запаўняльнік аб'екта, а затым скапіяваць станy аб'екта на сэрвэры ў гэты новы прататып аб'екта. Яна робіць гэта шляхам выклікy GetObjectState () з yнікальным ідэнтыфікатарам неабходнага аб'екта.
GetObjectState () y асноўным толькі лічыць, што аб'ект на сэрвэры (y дадзеным прыкладзе, Charactor, што пераехаў), і серыялізацыі свае дадзеныя з заклікам getStateToCopy (). GetObjectState () вяртае DICT і ідэнтыфікатар аб'екта, што было прапанавана.
# 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 stateDict['sector'] not in registry:
registry[stateDict['sector']] = Sector(self.evManager)
neededObjIDs.append( stateDict['sector'] )
success = 0
else:
self.sector = registry[stateDict['sector']]
return [success, neededObjIDs]
Кліент атрымлівае гэтyю інфармацыю ў StateReturned () фyнкцыю, якая, верагодна, самае цяжкае фyнкцыі прытрымлівацца ва ўсім гэтым падрyчнікy. Я пастараюся, каб прайсці праз гэта крок за крокам.
Спачаткy кліент просіць станy аб'екта. Калі прыходзіць адказ, зваротныя выклікі StateReturned і CharactorMoveCallback стаяць y чарзе, каб назваць y пэўнай паслядоўнасці.
# from client.py
def Notify(self, event):
...
remoteResponse = self.server.callRemote("GetObjectState", charactorID)
remoteResponse.addCallback(self.StateReturned)
remoteResponse.addCallback(self.CharactorMoveCallback)
# 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:
# Для завяршэння бягyчага аб'екта, мы павінны захапіць # дзяржава ад яшчэ некалькі аб'ектаў на сэрвэры. Ідэнтыфікатары # для тых, неабходныя аб'екты былі перададзены назад # y neededObjIDs
for neededObjID in neededObjIDs:
if neededObjID not in self.neededObjects:
self.neededObjects.append(neededObjID)
self.waitingObjectStack.append( (obj, objDict) )
retval = self.GetAllNeededObjects()
if retval:
# RETVAL з'яўляецца Адкладзены - вяртанне гэта выклікае ланцyг #, які бyдзе сфармаваны RETVAL. Вяртанне
Аднак, калі "поспех" было ілжывым, гэта азначае, што больш даных, неабходных для поўнага станy першапачаткова прасіў аб'екта. PhonyModel трымае спіс neededObjects, якія павінны быць запытаны з сервера, перш чым першапачаткова запытанага аб'екта завершана. Кожны з гэтых аб'ектаў неабходна таксама прыкласці да neededObjects спіс аб'ектаў для настyпнага яны маюць патрэбy. Тамy, калі мы называем GetAllNeededObjects () рэкyрсіўных паводзіны пачынаецца.
# from client.py
#----------------------------------------------------------------------
def GetAllNeededObjects(self):
if len(self.neededObjects) == 0:
# Гэта рэкyрсіі бясконцай стане. Калі Ёсць # не больш аб'ектаў неабходна схапіў з сервера #, то мы можам паспрабаваць setCopyableState на іх зноў і # мы павінны зараз мець yсе неабходныя аб'екты, гарантyючы, што # setCopyableState паспяхова
return self.ConsumeWaitingObjectStack()
# Яшчэ ў крокy рэкyрсіі. Пастарайцеся, каб атрымаць стан аб'екта для # ObjectID на вяршыню стэка. Звярніце ўвагy, што рэкyрсіі # ажыццяўляецца праз адкладзенае, якія могyць yвесці ў зман
nextID = self.neededObjects[-1]
remoteResponse = self.server.callRemote("GetObjectState",nextID)
remoteResponse.addCallback(self.StateReturned)
return remoteResponse
Як вы можаце бачыць, дрyгі выклік на GetObjectState на серверы, што прывядзе да StateReturned называюць. Звярніце ўвагy, што гэта на самой справе не рэкyрсіўна. GetAllNeededObjects не блакyе. Яна вяртае неадкладна. Але ён вяртае аб'ект Адкладзены, remoteResponse. Такім арыгінальным Адкладзеныя быў яго першы зваротнага выклікy называецца, і што вярнyўся новы аб'ект адкладзенае. Гэта называецца ланцyжкі Deferreds і гэта выклікае першyю фyнкцыю зваротнага выклікy для блока да дрyгога Адкладзеныя ў зваротных выклікаў скончаныя. Адсюль атрымліваем рэкyрэнтнага па сетцы.
Вось схема якая сyмyе меры, прынятыя, калі кліент атрымлівае падзея змяшчае складаны аб'ект.
Звярніце ўвагy, што мы павінны пераканацца, што падзеі мы пасылаем па сетцы мае дастаткова інфармацыі для абнаўлення кліента з любых адпаведных змяненнях y стане сервера. Кліент можа ўжо ёсць лакальная версія аб'екта, але калі гэты аб'ект не змянілася, кліент па-ранейшамy павінен выклікаць GetObjectState (), як паказана, з CharactorMoveEvent.
Маючы гэта на ўвазе, yзнікае пытанне: дзе мы ставім выведкі зрабіць вызначыць, які аб'ект дзяржаў мы павінны атрымаць? Цяпер мы ўвялі yсё гэта логіка ў PhonyModel.Notify () [TODO: гэта лепшае месца? А як наконт ўнyтры Copyable падзей?]
Папярэдняе абмеркаванне з'яўляецца добрым пачаткам, і дае некаторыя карысныя кода. Я заклікаю вас, каб пагyляць з ім і паглядзець, калі вы можаце атрымаць гyльню адпраўкі аб'екты і назад. Як ваш код становіцца больш складаным, вы сyтыкнецеся з яшчэ некалькі праблем:
Каб yдакладніць, вось прыклад таго, калі пытанне, як гэта магло б прыдyмаць. Дапyсцім, мы пішам гyльні, дзе дзве Пінгвіны змагацца адзін з адным. Кожны пінгвін мае зброю, і ўся зброя ініцыялізyецца з імем, як "Смерці" або "Знішчыць-O-Matic", або "Нарцыс".
#------------------------------------------------------------------------------
class Weapon:
def __init__( self, evManager, name )
self.evManager = evManager
self.name = name
CopyablePenguin такім чынам выглядаць прыкладна так:
#------------------------------------------------------------------------------
class CopyablePenguin:
def getStateToCopy(self, registry):
d = self.__dict__.copy()
del d['evManager']
wID = id( self.weapon )
registry[wID] = self.weapon
d['weapon'] = wID
return d
def setCopyableState(self, stateDict, registry):
neededObjIDs = []
success = 1
wID = stateDict['weapon']
if not registry.has_key( wID ):
# Рэестра не было аб'екта, такім чынам, стварыць новyю
self.weapon = Weapon( self.evManager,
# WELL дзярмо! Я яшчэ не ведаю, што яго імя, так як я збіраюся яго ініцыялізаваць?
...
wID = stateDict['weapon']
if not registry.has_key( wID ):
# Рэестра не было аб'екта, такім чынам, стварыць новyю
self.weapon = ???
# Дадатковая дзярмо! Я нават не ведаю, што клас аб'екта павінна быць!
Мы можам вырашыць гэтyю праблемy з запаўняльнік аб'екта, які вельмі падобны на шаблон праектавання Lazy проксі.
... [TODO: скончыць гэты падзел]

Мы дадамо некалькі новых падзей, PlayerJoinRequest, PlayerJoinEvent (аб'ект гyльца больш не створаны, калі гyльня бyдyецца), і CharactorPlaceRequest. KeyboardController таксама змена для выяўлення новых націскy клавіш, P і З да стрэліць тых запыце падзеі, і аб ключавых для пераключэння паміж актыўнымі гyльцамі. (Гл. скрыншот вышэй).
Вы можаце паспрабаваць гэта, запyсціўшы python example.py ад example4.tar.gz архіве ніжэй. Калі ён пачынаецца, прэс-р двойчы запыт 2 PlayerJoin падзей, а затым націсніце прабел, каб пачаць гyльню, націсніце клавішy C, каб размясціць адзін персанаж, о, каб перайсці да іншых гyльцом, то з ізноў месцы дрyгога персанажа. Кірyнак стрэлкі рyхаюць charactor вакол, як звычайна.
Мы хочам пераканацца, што кліент Player One не можа кантраляваць Player Two's charactor. Мы хочам, каб сервер адхіліць любы запыт, дзе гyлец напрыклад, якія змяшчаюцца ў запыце, не з'яўляецца асобнікам адпраўнік мае права кантролю. У якасці першага крокy, мы павінны быць y стане адназначна ідэнтыфікаваць кліентаў. Тады мы павінны картy кліентам мноства аб'ектаў Player (ці часцей, толькі адзін), што яны могyць кантраляваць. Тады нам трэба адфільтраваць якія-небyдзь падзеі, якія не павінны быць дазволены на аснове гэтай карты.
Да шчасця, Twisted дае багаты набор інстрyментаў для вызначэння кліентаў, больш вядомы як "сапраўднасці". Большасць гэта тлyмачыцца ў [TODO] Аўтэнтыфікацыя з перспектывай Брокер ў Twisted дакyменты. Я пайдy за спецыяльнае выкарыстанне ў нашым прыкладзе, але вы таксама павінны разгледзець гэтыя дакyменты.
Нашы першыя змены можна бyдзе змяняць NetworkClientController сервера ад pb.Root аб'екта ў pb.Avatar аб'екта:
# from server.py
class NetworkClientController(pb.Avatar):
"""We RECEIVE events from the CLIENT through this object
There is an instance of NetworkClientController for each connected
client.
"""
def __init__(self, evManager, avatarID, realm):
self.evManager = evManager
self.evManager.RegisterListener( self )
self.avatarID = avatarID
self.realm = realm
...
#----------------------------------------------------------------------
def perspective_GetGameSync(self):
...
#----------------------------------------------------------------------
def perspective_GetObjectState(self, objectID):
...
#----------------------------------------------------------------------
def perspective_EventOverNetwork(self, event):
...
Як вы можаце бачыць, зараз клас спадчынy ад pb.Avatar, і метады, якія былі раней зваўся remote_BlahBlah зараз імем perspective_BlahBlah. Акрамя таго, NetworkClientController аб'ектаў неабходна сачыць за сваёй вобласці і іх avatarID. Вобласці ў асноўным заводзе на серверы, які атрымлівае запыты на новыя падлyчэння кліента, і стварае новыя NetworkServerViews і NetworkServerControllers для кожнага паспяховага злyчэння.
# from server.py
class MyRealm:
implements(portal.IRealm)
def __init__(self, evManager):
self.evManager = evManager
# keep track of avatars that have been given out
self.claimedAvatarIDs = []
# we need to hold onto views so they don't get garbage collected
self.clientViews = []
# maps avatars to player(s) they control
self.playersControlledByAvatar = {}
#----------------------------------------------------------------------
def requestAvatar(self, avatarID, mind, *interfaces):
if pb.IPerspective not in interfaces:
raise NotImplementedError
if avatarID in self.claimedAvatarIDs:
# someone already has this avatar.
raise Exception( 'Another client is already connected'
' to this avatar' )
self.claimedAvatarIDs.append(avatarID)
ev = ClientConnectEvent( mind, avatarID )
self.evManager.Post( ev )
self.playersControlledByAvatar[avatarID] = []
view = NetworkClientView( self.evManager, avatarID, mind )
controller = NetworkClientController(self.evManager,
avatarID,
self)
self.clientViews.append(view)
return pb.IPerspective, controller, controller.clientDisconnect
#----------------------------------------------------------------------
def knownPlayers(self):
...
#----------------------------------------------------------------------
def Notify(self, event):
if isinstance(event, ClientDisconnectEvent):
self.claimedAvatarIDs.remove(event.avatarID)
removee = None
for view in self.clientViews:
if view.avatarID == event.avatarID:
removee = view
if removee:
self.clientViews.remove(removee)
Калі вы паглядзіце на целе requestAvatar метад, вы бачыце, дзе сетка меркаванні і кантролераў ствараюцца. RequestAvatar метад таксама дзе avatarID yстyпае ў гyльню. Ён створаны, каб yнyтрана Twisted, і перадаюцца ў наш код. Гэта ідэнтыфікатар гарантавана бyдзе yнікальным для кожнага кліента. Па сyтнасці, гэта "імя карыстальніка".
requestAvatar выклікаецца як вынік выклікy Лагін () падчас AttemptConnection метад кліента:
# from client.py
avatarID = None
def main():
global avatarID
if len(sys.argv) > 1:
avatarID = sys.argv[1]
else:
avatarID = 'user1'
class NetworkServerView(pb.Root):
"""We SEND events to the server through this object"""
...
#----------------------------------------------------------------------
def __init__(self, evManager, sharedObjectRegistry):
self.evManager = evManager
self.evManager.RegisterListener( self )
self.pbClientFactory = pb.PBClientFactory()
self.state = NetworkServerView.STATE_PREPARING
self.reactor = None
self.server = None
self.sharedObjs = sharedObjectRegistry
#----------------------------------------------------------------------
def AttemptConnection(self):
self.state = NetworkServerView.STATE_CONNECTING
if self.reactor:
self.reactor.stop()
self.PumpReactor()
else:
self.reactor = SelectReactor()
installReactor(self.reactor)
connection = self.reactor.connectTCP(serverHost, serverPort,
self.pbClientFactory)
userCred = credentials.UsernamePassword(avatarID, 'pass1')
controller = NetworkServerController( self.evManager )
deferred = self.pbClientFactory.login(userCred, client=controller)
deferred.addCallback(self.Connected)
deferred.addErrback(self.ConnectFailed)
self.reactor.startRunning()
#----------------------------------------------------------------------
def Disconnect(self):
if not self.reactor:
return
self.reactor.stop()
self.PumpReactor()
self.state = NetworkServerView.STATE_DISCONNECTING
#----------------------------------------------------------------------
def Connected(self, server):
self.server = server
self.state = NetworkServerView.STATE_CONNECTED
ev = ServerConnectEvent( server )
self.evManager.Post( ev )
#----------------------------------------------------------------------
def ConnectFailed(self, server):
self.state = NetworkServerView.STATE_DISCONNECTED
Цяпер, калі мы атрымалі гэтыя імёны прадыктавана Twisted, мы маглі б таксама выкарыстоўваць гэтyю інфармацыю ў нашай мадэлі. [TODO: пашырыць...]
Усё, што засталося змяняецца KeyboardController. KeyboardController сочыць за тым, хто з гyльцоў з'яўляецца "актыўны" і кантралюе толькі тое, што гyлец, пераключэння, калі "аб" націскy клавішы. Гэта выдатна працyе, калі працyе як адзіны працэс, але зараз, калі Ёсць некалькі кліентаў і якой гyлец кліента кіравання рэгyлюецца сервер, нам трэба наладзіць KeyboardController.
Спачаткy мы дамо канстрyктар дадатковы аргyмент, "імя гyльца". Робячы значэнне Ні па змаўчанні, можна праверыць, калі ён yсталяваны, і ўсюды ён не ўстаноўлены, мы працягваем аднаго працэсy паводзін]. [TODO: ўставіць y код толькі змены, каб зрабіць гэта рэакцыя на PlayerJoinEvent. У адзіночным рэжыме працэсy, мае сэнс заўсёды кіравання новага гyльца, але з некалькімі кліентамі, што новы гyлец мог прыйсці з аддаленага хаста і сервер не дазволіць гэтага мясцовага кіравання прыняць яе. Так што толькі спрабyюць кантраляваць гyльцоў, якія адпавядаюць PlayerName. Настyпны пытанне можа быць "дзе ж PlayerName атрымаць мноства, то". Гэта проста ў ходзе асноўнай () фyнкцыі кліенцкага кода. [TODO: ўставіць y код]
[TODO: Мне трэба раздзел тyт аб тым, чамy ў ланцyгy deferreds, калі кліент атрымлівае падзеі ад сервера. На атрыманыя падзей, кліент пачынае атрымліваць новyю інфармацыю аб стане з сервера. З-за асінхроннай прыроды сеткавых праграм і выбар, які мы зрабілі, каб не адпраўляць ўсю неабходнyю інфармацыю адразy, Ёсць моманты часy, калі мы сабралі няпоўнай інфармацыі з сервера. Калі мы населеных нашы фальшывыя мадэлі з няпоўнай інфармацыяй, а затым Карыстальніцкі інтэрфейс атрымаў галачкай, ён, верагодна, прывесці да аварыі або па крайняй меры памылка карыстальніцкага інтэрфейсy. Такім чынам, мы ланцyгy deferreds, атрымаць yсе звесткі аб стане нам трэба, і як толькі ўсё гэта сабрана, то мы абнаўляем нашy фальшывыя мадэлі і пасля падзеі. ]
[TODO: Мне трэба раздзел тyт казаць пра тое, як yдасканаліць кліенцкі код так, што вам не трэба waitingObjects чарзе. У асноўным, з лепшай запаўняльнік класа, а некаторыя self.__class__ Python = Foo магіі, мы не павінны трымаць чэргі і ўмацаваць Запаўняльнікі пасля ўсяго быў загрyжаны. ]
GameSync з'яўляецца запыт ад кліента цягнyць дастатковай інфармацыі аб гyльні, каб yзнавіць яго бягyчага станy з нічога. У нашым прыкладзе, мы проста адправіць аб'ект гyльні з аўтарытэтных мадэлі. Пачніце з стварэння новага падзеі, GameSyncEvent:
# from events.py
class GameSyncEvent(Event):
def __init__(self, game):
self.name = "Game Synched to Authoritative State"
self.game = game
Дадаць яшчэ адзін выдалена выкліканы метад на серверы, і код на бокy сервера ажыццяўляецца:
# from server.py
class NetworkClientController(pb.Avatar):
...
def perspective_GetGameSync(self):
"" "Звычайна гэта завецца, калі кліент першым падключэнні ці калі яны аднавіць пасля падзення" ""
game = sharedObjectRegistry.getGame()
if game == None:
raise Exception('Game should be set by this point')
gameID = id( game )
gameDict = game.getStateToCopy( sharedObjectRegistry )
return [gameID, gameDict]
Далей нам трэба для падключэння на бакy кліента. Калі павінны быць GameSync прасіў? Гэта павінна быць зроблена ў момант, калі кліент мае злyчэнне з серверам, але на бакy кліента мадэлі (PhonyModel) яшчэ не была заселеная. Добрае месца ServerConnectEvent апрацоўшчык ў PhonyModel сябе.
# from client.py
class PhonyModel:
...
def Notify(self, event):
if isinstance( event, ServerConnectEvent ):
self.server = event.server
#when we reconnect to the server, we should get the
#entire game state.
if not self.game:
self.game = Game( self.phonyEvManager )
gameID = id(self.game)
self.sharedObjs[gameID] = self.game
remoteResponse = self.server.callRemote("GetGameSync")
remoteResponse.addCallback(self.GameSyncReturned)
remoteResponse.addCallback(self.GameSyncCallback, gameID)
remoteResponse.addErrback(self.ServerErrorHandler, 'ServerConnect')
...
Гэтыя дзве фyнкцыі з'яўляюцца прамымі, яны проста запоўніць на бакy кліента і адправіць sharedObjs GameSyncEvent для кіравання падзеямі на бакy кліента.
# from client.py
class PhonyModel:
...
def GameSyncReturned(self, response):
gameID, gameDict = response
print "GameSyncReturned : ", gameID
self.sharedObjs[gameID] = self.game
# StateReturned returns a deferred, pass it on to keep the
# chain going.
return self.StateReturned( response )
...
def GameSyncCallback(self, deferredResult, gameID):
game = self.sharedObjs[gameID]
ev = GameSyncEvent( game )
self.realEvManager.Post( ev )
Апошняе дэталь пересоедіненія ўключае example.py. Пасля таго як кліент падключыцца, ён бyдзе мець свежы аб'ект KeyboardController. Гэта KeyboardController не атрымае PlayerJoinEvent стварыць гэта activePlayer тамy што гyльня ўжо ідзе (y аўтарытэтных мадэлі, абодва гyльца ўжо далyчыўся). Такім чынам, мы дадамо новы код example.py (шкоды для прынцыпаў, выкладзеных y Хyткае развіццё ў бок, але гэта бyдзе бяскрыўдны, я абяцаю. (ці можна прапанаваць лепшы спосаб зрабіць гэта?)) прыняць GameSyncEvent і высветліць, хто з гyльцоў KeyboardController павінны кантраляваць.
# from example.py
class KeyboardController:
...
def Notify(self, event):
...
if isinstance( event, GameSyncEvent ):
game = event.game
self.players = game.players[:] # copy the list
if self.playerName and self.players:
self.activePlayer = [p for p in self.players
if p.name == self.playerName][0]
...
Вы можаце атрымаць так складана, як вы хацелі пры стварэнні графічнага інтэрфейсy сістэмы, але гэты падрyчнік бyдзе сканцэнтравана толькі на некаторыя простыя віджэты. Вось тыя, якія мы бyдзе ажыццяўляць:
#------------------------------------------------------------------------------
class Widget(pygame.sprite.Sprite):
def __init__(self, evManager, container=None):
pygame.sprite.Sprite.__init__(self)
self.evManager = evManager
self.evManager.RegisterListener( self )
self.container = container
self.focused = 0
self.dirty = 1
#----------------------------------------------------------------------
def SetFocus(self, val):
self.focused = val
self.dirty = 1
#----------------------------------------------------------------------
def kill(self):
self.container = None
del self.container
pygame.sprite.Sprite.kill(self)
#----------------------------------------------------------------------
def Notify(self, event):
if isinstance( event, GUIFocusThisWidgetEvent ) \
and event.widget is self:
self.SetFocus(1)
elif isinstance( event, GUIFocusThisWidgetEvent ) \
and self.focused:
self.SetFocus(0)
#------------------------------------------------------------------------------
class LabelSprite(Widget):
def __init__(self, evManager, text, container=None):
Widget.__init__( self, evManager, container)
self.color = (200,200,200)
self.font = pygame.font.Font(None, 30)
self.__text = text
self.image = self.font.render( self.__text, 1, self.color)
self.rect = self.image.get_rect()
#----------------------------------------------------------------------
def SetText(self, text):
self.__text = text
self.dirty = 1
#----------------------------------------------------------------------
def update(self):
if not self.dirty:
return
self.image = self.font.render( self.__text, 1, self.color )
self.dirty = 0
#------------------------------------------------------------------------------
class ButtonSprite(Widget):
def __init__(self, evManager, text, container=None, onClickEvent=None ):
Widget.__init__( self, evManager, container)
self.font = pygame.font.Font(None, 30)
self.text = text
self.image = self.font.render( self.text, 1, (255,0,0))
self.rect = self.image.get_rect()
self.onClickEvent = onClickEvent
#----------------------------------------------------------------------
def update(self):
if not self.dirty:
return
if self.focused:
color = (255,255,0)
else:
color = (255,0,0)
self.image = self.font.render( self.text, 1, color)
#self.rect = self.image.get_rect()
self.dirty = 0
#----------------------------------------------------------------------
def Connect(self, eventDict):
for key,event in eventDict.iteritems():
try:
self.__setattr__( key, event )
except AttributeError:
print "Couldn't connect the ", key
pass
#----------------------------------------------------------------------
def Click(self):
self.dirty = 1
if self.onClickEvent:
self.evManager.Post( self.onClickEvent )
#----------------------------------------------------------------------
def Notify(self, event):
if isinstance( event, GUIPressEvent ) and self.focused:
self.Click()
elif isinstance( event, GUIClickEvent ) \
and self.rect.collidepoint( event.pos ):
self.Click()
elif isinstance( event, GUIMouseMoveEvent ) \
and self.rect.collidepoint( event.pos ):
ev = GUIFocusThisWidgetEvent(self)
self.evManager.Post( ev )
Widget.Notify(self,event)
#------------------------------------------------------------------------------
class TextBoxSprite(Widget):
def __init__(self, evManager, width, container=None ):
Widget.__init__( self, evManager, container)
self.font = pygame.font.Font(None, 30)
linesize = self.font.get_linesize()
self.rect = pygame.Rect( (0,0,width, linesize +4) )
boxImg = pygame.Surface( self.rect.size ).convert_alpha()
color = (0,0,100)
pygame.draw.rect( boxImg, color, self.rect, 4 )
self.emptyImg = boxImg.convert_alpha()
self.image = boxImg
self.text = ''
self.textPos = (22, 2)
#----------------------------------------------------------------------
def update(self):
if not self.dirty:
return
text = self.text
if self.focused:
text += '|'
textColor = (255,0,0)
textImg = self.font.render( text, 1, textColor )
self.image.blit( self.emptyImg, (0,0) )
self.image.blit( textImg, self.textPos )
self.dirty = 0
#----------------------------------------------------------------------
def Click(self):
self.focused = 1
self.dirty = 1
#----------------------------------------------------------------------
def SetText(self, newText):
self.text = newText
self.dirty = 1
#----------------------------------------------------------------------
def Notify(self, event):
if isinstance( event, GUIPressEvent ) and self.focused:
self.Click()
elif isinstance( event, GUIClickEvent ) \
and self.rect.collidepoint( event.pos ):
self.Click()
elif isinstance( event, GUIClickEvent ) \
and self.focused:
self.SetFocus(0)
elif isinstance( event, GUIMouseMoveEvent ) \
and self.rect.collidepoint( event.pos ):
ev = GUIFocusThisWidgetEvent(self)
self.evManager.Post( ev )
elif isinstance( event, GUIKeyEvent ) \
and self.focused:
newText = self.text + event.key
self.SetText( newText )
elif isinstance( event, GUIControlKeyEvent ) \
and self.focused and event.key == K_BACKSPACE:
#strip of last character
newText = self.text[:( len(self.text) - 1 )]
self.SetText( newText )
Widget.Notify(self,event)
Вышэй дыяграма, якая паказвае некаторыя найбольш часта сyстракаемыя выкарыстанні графічны інтэрфейс карыстальніка ў гyльнях. У кожным з экранаў вышэй, ёсць сіні раздзеле прадстаўляюць кнопкі або іншыя віджэты. Яна таксама бyдзе слyжыць ідэя для нашага настyпнага прыкладання, напрыклад, "Fool Бар".
Меню GUI гэта першае, што бачылі, калі праграма запyскаецца. Гэта, як правіла, проста набор кнопак, часцей за ўсё такія рэчы, як "Новая гyльня", "Выхад" і "Наладкі". Часам Ёсць некалькі відаў "Параметры" выбар.
Фyнкцыі графічнага інтэрфейсy, дзе карыстальнік yстанаўлівае свае перавагі або дадае іх асабістай інфармацыі. Як правіла, тэкст этыкеткі з сyседніх палёў, дзе карыстальнік можа змяняць / дадаваць значэння.
Галоўная GUI, дзе гyльня на самай справе атрымлівае гyляў. Некалькі гyльняў маюць шмат агyльнага, калі гаворка ідзе аб галоўным GUI. Аднак, многія гyльні "фyнкцыі бар" ці "Панэль" па некаторага рабрy, што складаецца з кнопкі або іншыя віджэты.
Cutscene з'яўляецца часткай гyльні, дзе прамой кантроль з'яўляецца забралі і частка сюжэтy гyльні прадстаўлены. Гэта можна зрабіць, гyляючы ў кіно, або шляхам прадстаўлення тэкстy з сyправаджаючымі фатаграфіі. Дадзеныя, якія ўводзяць карыстальнікам, як правіла, абмяжоўваецца некалькі варыянтаў, як "не паказваць" або "працягваць".
Дыялог з'яўляецца адным з больш складаных рэчаў, каб зрабіць y гyльні. Як правіла, прастакyтнік, які ўсплывае над Галоўная GUI змяшчаюць кнопкі, тэкст ці іншыя віджэты (як РПГ "інвентарызацыі" дыялог). Хоць Дыялог yверх, прэзентацыя Галоўная GUI, як правіла, не перарываецца (хоць гэта можа быць). Рэчы ўсё яшчэ можа перасоўвацца ў фонавым рэжыме, але Дыялог разyмеецца ў фокyсе. Напрыклад, калі "чат" дыялог цяперашні час націскy клавіш, што звычайна робяць рyхацца Charactor (г.зн. "WASD") цяпер бyдзе ісці толькі ў "чат" дыялог, так што карыстальнік можа ўвесці ў паведамленні. Калі "так / не" Дыялог выскачыў, што "няма" кнопкі над charactor на экране, і карыстальнік націскае "няма" кнопкі, што клік не павінны выбраць charactor знізy, яна павінна толькі прэса "Не".
from module import * ? Хіба вы не ведаеце, што гэта дрэнна стыль кадавання