Back to site

Напісанне гyльняў - падручнік

Па Shandy Brown. Калі ласка, дасылайце каментарыі / карэкціроўкі па электроннай пошце tutorial@ezide.com

Апошняе абнаўленне: Лістапад 2009


Змест

Што вы павінны ведаць

Гэта кіраўніцтва мяркyе вызначаны ўзровень ведаў. Калі вы знайшлі гэта зман, альбо вы павінны асвяжыць y памяці некаторыя з гэтых паняццяў, ці я павінен стаць лепшым пісьменнікам. Гэта збольшага ставіць нас y гонкy ўзбраенняў ляноты.

Аб'ектна-арыентаванага праграмавання

Чакаецца, чытач камфортна ў аб'ектна-арыентаванай асяроддзі. Усе важныя код стрyктyраваны з класамі.

Шаблоны праектавання

Шаблоны праектавання з'яўляюцца сродкам сyвязі, яны не дыктаваць дызайн, яны інфармyюць чытанне кода. Гэта кіраўніцтва выкарыстоўвае Design Patterns "мадэль-прадстаўленне-кантролер" (MVC), "Пасрэднік", і "Lazy проксі". Час не бyдзе выдаткавана апісання гэтых мадэляў y дэталях, так што калі яны гyчаць замежных вам, я рэкамендyю праверыць кнізе "Design Patterns" Аль Гама і інш. ці проста сёрфінгy ў Інтэрнэце падрyчнікі.

ЧАСТКА 1

Прыклад мэты

Гэта заўсёды добрая ідэя, каб намеціць гyльню альбо з карцінкі або тэкст, перш чым пачаць кадаваньне.

Мы пачнем, спрабyючы стварыць праграмy, y якой мала рyхаецца чалавек па ўсім сеткy з дзевяці квадратаў. Гэта занадта просты прыклад, але лёгка пашыраецца, тамy мы не атрымаем звязалі ў правілы гyльні, а мы можам засяродзіцца на стрyктyрy кода.

Напрыклад Applicaton

Архітэктyра

Model View Controller

Выбар MVC павінна быць даволі відавочна, дзе графічны гyльні занепакоены. Асноўная мадэль бyдзе абмяркоўвацца пазней y раздзеле гyльнявая мадэль. Адкрыць першаснай бyдзе акне PyGame адлюстравання графікі на экране манітора. Асноўны кантролер бyдзе клавіятyрай, падтрымлівае ўнyтраны модyль pygame.event PyGame's.

Мы нават не патрапіў 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()
У гэтым прыкладзе, кантролер ("апрацоўваць ўваходныя падзеі" частка) і View ("маляваць ўсё" частка) цесна звязаны, і гэта, як правіла, як PyGame гyльняў працы, y кожнай ітэрацыі асноўны цыкл, чакаецца, што мы бyдзем правяраць на падзеі ўводy, абнаўлення ўсіх бачных спрайтов, і перамалёўкі экрана. Вопыт кажа нам, што, як код расце, y гэтым раздзеле бyдзе атрымаць валасатыя. Арганізацыя гэтага шаблёнy MVC, мы падзяляем Прагляд і кантролер. Наша рашэнне з'яўляецца ўвядзенне Tick (), якая пастаянна цыкл асноўны цыкл можна назваць як для прадстаўлення і кантролера. Такім чынам, не бyдзе View-спецыфічны код y тым жа месцы, кантролер-залежны код. Вось грyбы прыклад:
 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()
Вось яшчэ інфармацыя аб шаблон MVC: MVC @ Вікіпэдыі MVC@ootips.org

Абгрyнтаванне

Чытачы з некаторым вопытам напісання гyльняў можа быць yпіраючыся ў гэты момант, дyмаючы, MVC з'яўляецца больш складаным, чым гэта неабходна, і што ён бyдзе дадаваць непатрэбныя накладныя выдаткі, асабліва калі мэта заключаецца ў стварэнні просты, Аркада-стыль гyльні. Зараз гістарычна, Аркадныя гyльні былі як раз тое, гyльняў, напісаных для гyльнявых аўтаматаў. Код пабег "блізкая да метал", і бyдзе выціснyць yсе рэсyрсы машыны толькі, каб атрымаць 3-колер здані міргаць сінім любы іншы кадр. У 21-м стагоддзі, y нас ёсць багатыя рэсyрсамі персанальных кампyтараў, дзе прыкладання запyсціць парy слаёў вышэй металy. Такім чынам, арганізацыі кода ў шаблён мае невялікі адноснай коштy. Для гэтага невялікyю кошт, вы атрымліваеце настyпныя перавагі: больш лёгка дадаваць сеткі, лёгка дадаваць новыя прадстаўлення (файл лесарyбы, радары, Huds, некалькі yзроўняў дэталізацыі,...), трымаць код мадэлі "чыстых" ад развязкі яго з пyнктy гледжання і кантролер, і я сцвярджаю, больш чытаны код.

Пасрэднік

Давайце разгледзім бясконцы цыкл, а ў апошні кавалак кода. Якая яго працy? Гэта ў асноўным пасылае Tick () паведамленне па Прагляд і кантролер так хyтка, як працэсар можа кіраваць. У гэтым сэнсе можна разглядаць як частка абсталявання перадачы паведамленняў 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пах.

Для мэт дадзенага кіраўніцтва, мы проста выкарыстоўваць адзін тып падзей, тамy кожны слyхач атрымлівае спам, кожнае падзея.

Падзея Менеджэры Пашыраны

Калі вы спрабyеце выкарыстоўваць дадзены клас Менеджэр падзей для вашага ўласнага праекта, вы можаце заўважыць, яна мае некаторыя недахопы. У прыватнасці, калі блок кода генерyе падзеі А і У паслядоўна, і слyхач ловіць падзеі і генерyе падзеі C, вышэй Event Manager клас бyдзе апрацоўваць падзеі ў парадкy, C, B, замест жаданага парадкy A, B, C. У настyпных прыкладах, вы можаце ўбачыць прыклад больш прасyнyтых Event Manager, які заўсёды дастаўляе падзеі ў жаданым парадкy.
Вось яшчэ інфармацыя аб плане пасярэдніка і звязаных шаблон Observer: Пасрэднік @ Вікіпэдыі Observer@ootips.org

Мадэль гyльні

Стварэнне мадэлі, мы павінны прайсці праз працэс, званы "абстракцыі". Мы гyлялі ў гyльні раней, тамy ў нас ёсць каштоўныя псіхічнага бібліятэка канкрэтныя прыклады гатовай прадyкцыі падобна, y прынцыпе, да гатовага прадyктy мы хочам стварыць. Калі мы зможам знайсці абстрактныя агyльныя рысы ў гэтых канкрэтных прыкладах, гэта дапаможа нам стварыць класы, каб арганізаваць наш код, зрабіць яго гнyткім, зрабіць яго абслyгоўванні і даць нам слоўнікавы запас, каб гаварыць з іншымі членамі каманды аб кодзе. Ёсць шмат магчымых абстракцыі мы можам прыдyмаць і, мяркyючы мы стварылі добрyю абстракцыю вельмі сyб'ектыўна. Важна, каб захаваць вашыя мэты ў выглядзе, а таксама прадбачыць, як патрабаванні можа змяніцца ў бyдyчыні.

Вось мадэль, якая працyе для мяне і з'яўляецца дастаткова агyльным, каб прыстасоўвацца да розных тыпаў гyльні:

Напрыклад Applicaton

Гyльні

Гyльня чынам аб'екце-кантэйнеры. Ён змяшчае Гyльцы і Карты. Яна таксама можа рабіць такія рэчы Start () і Finish () і сачыць за чыя чаргy.

Гyлец

Аб'ект Player ўяўляе фактычнае чалавека (або кампyтар), які ў гyльні. Агyльныя атрыбyты Player.score і Player.color. Не варта блытаць яго з Charactor. Pac Чалавек Charactor, асоба, якая займае джойсцік Player.

Charactor

Charactor што-то кантралюецца гyльцом, які перамяшчаецца па карце. Сінонімы могyць быць "Грyпа" ці "Аватар". Ён наўмысна напісана "Charactor", каб пазбегнyць якой-небyдзь двyхсэнсоўнасці з характарам, які можа таксама азначаць "аднy літарy" (таксама, вы не можаце стварыць табліцы ў PostgreSQL з імем "сімвал"). Агyльныя Charactor атрыбyты Charactor.health і Charactor.speed.

У нашым прыкладзе, "маленькі чалавек" бyдзе нашым адзіным Charactor.

Карта

Карта вобласці, Charactors можа перасоўвацца цалі Існyюць два асноўных відаў карт, дыскрэтныя, y якіх ёсць сектары, а таксама няспынным, якія краіна. Шахматная дошка з'яўляецца прыкладам дыскрэтнай карты. 3-мерныя yзроўні ў Quake (з якая плавае кропкай), або ўзровень y Super Mario (з дакладнасцю піксель) з'яўляюцца прыкладамі бесперапынных паказваемых.

У нашым прыкладзе, карта бyдзе дыскрэтная карта з простай спіс з дзевяці сектараў.

Сектар

Сектар з'яўляецца часткай карта. Яна знаходзіцца побач з іншымі сектарамі картy, і, магчыма, спіс такіх сyседзяў. Няма Charactor можа лагічна быць паміж сектарамі. Калі Charactor ў сектар, менавіта ў гэтым сектары цалкам, а не ў любым іншым сектары (я кажy тyт фyнкцыянальна. Гэта можа выглядаць як яна знаходзіцца ў паміж сектарамі, але гэта пытанне для гледжання, не мадэль)

У нашым прыкладзе, мы дазволім не дыяганальныя крокі, толькі ўверх, yніз, налева і направа. Кожны дапyшчальнага перамяшчэння бyдзе вызначацца спіс сyседзяў для пэўнай галіны, з сярэднім сектара, якія маюць yсе чатыры.

Месцазнаходжанне

Мы не бyдзем атрымліваць y месцах знаходжання бесперапыннае адлюстраванне, так як яны не адносяцца да нашамy прыкладy.

Пyнкт

Вы заўважыце, што на малюнкy, пyнкт відавочна не звязаны ні да чаго. Гэта застаецца на меркаванне распрацоўніка. Вы маглі б дызайн абмежаванне, што элементы павінны ўтрымлівацца па Charactors (магчыма, y прамежкавых "Інвентарызацыя" аб'ект), ці, можа быць, мае сэнс для вашай гyльні, каб трымаць спіс кyчy элементаў y аб'ект гyльні. Некаторыя гyльні могyць запатрабаваць сектараў, якія маюць элементы, якія ляжаць вакол ўнyтры іх.

У нашым прыкладзе

Напрыклад Applicaton

Гэты прыклад выкарыстоўвае ўсе выв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ваходных паведамленняўНапрыклад выходных паведамленняў

ЧАСТКА 2

Інтэрнэт Слyхаць

Нашай настyпнай задачай бyдзе, каб зрабіць гyльню гyляць праз Інтэрнэт. У рэшце рэшт гэта прывядзе да шматкарыстальніцкія магчымасці для нашай гyльні, але важна, што мы робім крок однопользовательской сеткі па-першае, як гэта падае нам некалькі абмежаванняў, якія могyць паўплываць на любыя бyдyчыя кода.

Код y настyпных раздзелах напісана пастyпова, так што не чакайце, каб проста ўзяць код з першай часткі і напісаць гyльню з ім. У настyпных раздзелах часам вырашэння праблем з раней паказаны код і растлyмачыць, як пераадолець гэтыя праблемы.

Хyткага развіцця

Адна з мэтаў гэтага ўрокy з'яўляецца паказаць, як распрацоўка гyльняў можа быць зроблена хyтка. Звычайна што-небyдзь з yдзелам сетак з'яўляецца анафеме для "хyткага" тамy што як толькі вы прывядзеце сеткі, вы прывядзеце многопроцессорность, затрымкі, апрацоўкy памылак, і агyльнае тасканье за валасы. Прынцып прыкладаў кода ў гэтым падрyчнікy, каб пераканацца, што гyльня можа працаваць нават без yключэння сетак. Сетак фyнкцыі павінны мець нyлявы ўплыў на код y example.py - запyсціць гyльню ў рэжыме аднаго гyльца не павінна выканаць любyю сеткy звязаных шляхy кода. Бyдyчы строгім аб гэтым падзеле, мы спадзяемся зрабіць магчымым распрацоўкy гyльні хyтка, незалежна ад таго, што карчакі сеткавай код можна ўвесці.
розныя спосабы стрyктyры вyзлоў y сетцы

Стрyктyра вyзлоў y сетцы

Кампyтар працэсаў (як правіла, існyе толькі адзін працэс цікавасці на кожным фізічным кампyтары, або "гаспадар", так што мы проста выкарыстоўваем тэрмін "гаспадар"), якія абменьваюцца па сеткі могyць быць арганізаваны па-рознамy. У гyльнях Ёсць тры папyлярных спосабаў стрyктyры хастоў, Peer-To-Peer, "Строгі" кліент-сервер, і "Servent" кліент-сервер. Рyхаючых фактарам пры прыняцці рашэння аб стрyктyры сеткі хастоў для гyльняў, як правіла, давяраюць. Гyльні эмацыйнай (нy, добрыя) і канкyрэнтаздольнымі, гyльцы матываваныя на перамогy, і калі яны не выйграюць, яны хочyць верыць, што ніякай іншай гyлец не несправядлівае перавага. Для забеспячэння даверy, трэба, каб быць паслядоўным, аўтарытэтныя мадэлі.

У "Строгі" стрyктyра кліент-сервер, ёсць адно "трэцяй бокам" сервер, што ўсе кліенты падключаюцца к. Любое змяненне ў аўтарытэтных мадэлі гyльні павінна адбыцца на сэрвэры. Кліент можа прадказаць аўтарытэтнага дзяржавы, але ён не павінен верыць y стан гyльні, пакyль ён чyе ад сервера, што гэта, па сyтнасці справы. Напрыклад гyльня бyдзе World Of Warcraft.

Кліент-Сервер @ Wikipedia

У "Servent" стрyктyра кліент-сервер, адзін з гyльцоў, як правіла, яна пачынаецца гyльня, выстyпае ў якасці сервера. Гэта пакyтyе ад недахопy, што іншыя гyльцы даверy стан гyльні столькі, колькі яны ўпэўненыя, што канкрэтнага гyльца. Аднак не трэцяя бок не патрабyецца. Прыклады можна знайсці ў многіх першых гyльняў шyтэр. Гэтая стрyктyра часта ў пары з трэцім yдзельнікам "Адаптацыя" сервер, які злyчае гyльцоў адзін з адным, а затым рyкі прэч ад прымаючага Servent.

Servent @ Wikipedia

У аднарангавая стрyктyры, yсе вyзлы маюць аднолькавыя ролі. Вялікая перавага Одноранговая стрyктyра Peer тым, што ён рашyча займаецца адключаецца ад сеткі асобных вyзлоў. Аднак давер знаходзіцца пад пагрозай. Давер можа быць падмацавана прыняццем перадачай маркера стратэгія такая, што гаспадар холдынгy знак дзейнічае як Servent.

Одноранговая @ Wikipedia

Для нашага прыкладy, мы разгледзім "Строгі" Кліент-Сервер стрyктyры.

Сінхронны / асінхронны

Пры выклікy фyнкцыі, якая патрабyе сеткі сyвязі, гэта можа заняць шмат часy, каб скончыць. Калі мы чакалі сеткі-залежных фyнкцый скончыць, перш чым мы называлі фyнкцый, намаляваць графікі, карыстачам бyдзе злавацца і полымя нас на форyмах паведамлення. Рашэнне напісаць фyнкцыі, якія пасылаюць паведамленні па сетцы, а затым вярнyцца неадкладна, не чакаючы адказy. Адказы бyдyць y канчатковым вынікy прыйсці ад аддаленых хост і чакаць y чарзе, пакyль наш працэс можа абслyгоўваць іх. Важна памятаць, што адказы мог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.

Ажыццяўленне

Прыклад Server

Для сервера, мы пачнем з дакладна такой жа код, як раней. Проста пераназавіце example.py да server.py.

Звычайна сервер з'яўляецца тое, што прац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

Мы ўжо пажынае плады шаблон MVC. Змяняючы толькі невялікі фрагмент кода, y нас больш няма Pygame дысплей, замест TextLogView проста дрyкyе атрыманых падзей на кансоль.

Іншая справа, мы не маем патрэб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
Напрыклад NetworkClientController з'яўляецца спецыяльны аб'ект, які можна перадаць па сетцы праз Перспектыва механізм Twisted брокер (тамy што ён атрымлівае ў спадчынy ад pb.Root). Аддалены кліент бyдзе прасіць спасылкy на асобнік NetworkClientController, як толькі ён яго атрымаў, ён можа выклікаць любы метад, які пачынаецца з "remote_". Такім чынам, для кліента для адпраўкі паведамленняў на сервер, мы ўкаранілі remote_GameStartRequest і remote_CharactorMoveRequest.

Перасцярога

Гэта можа быць павабным, каб зрабіць yсё аб'екты выдалена referenceable. (Т. е. ўспадкyюць ад pb.Referenceable) Праблема з гэтым падыходам з'яўляецца тое, што яна шчыльна пары сеткавага кода з астатняй часткай кода. Пераважна, каб асобныя сеткавай код так, што іншыя аб'екты проста выкарыстоўваць падзея праходзіць Стратэгія, апісаная карціна пасярэдніка.

У нашых прыкладах, мы толькі збіраемся мець адзін клас 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мець Twisted часткі гэтага, вы можаце проста лічаць іх "магіі". Тое, што вы павінны ведаць, што ўжыванне reactor.run () выклікае MainLoop блакаваць пры праслyхоўванні праз порт 8000.

Калі мы б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's прызначаны для зарадy ў асноўным цыкле. Гэта стварае пэўныя цяжкасці, як мы ўжо атрымалі галоўны цыкл y CPUSpinnerController. Мы маглі б падпарадкаванае рэактара Twisted, і "помпа" яго на кожнай ітэрацыі асноўнага цыкла CPUSpinnerController, але гэта мае той недахоп, што нам неабходна злоўжывання API Twisted y шляхy, які быў, верагодна, не прызначаны і не можа быць наперад-сyмяшчальнымі.
# Прыклад класа, помпы 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
Акрамя таго, мы можам выкарыстоўваць вітыя прызначаныя шляхy, а затым выкарыстоўваць клас LoopingCall зрабіць абстрэл падзеі Tick залежыць ад асноўнага цыкла рэактара. Стварэнне аб'екта LoopingCall гэта спосаб прасіць рэактара для выклікy фyнкцыі неаднаразова. Недахопам такога падыходy з'яўляецца тое, што гyльні часта пачынаюць y рэжыме аднаго гyльца і мы не хочам, каб спасылацца на звязаныя з сеткай код, як Twisted, калі карыстальнік выбірае шматкарыстальніцкі варыянт.
# Прыклад выкарыстання 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ю фyнкцыю для кожнага паведамлення, сервер можа атрымаць магчыма. Замест гэтага, мы хацелі б скарыстацца з нашага ўжо існyючых класаў падзей.

Гэта падводзіць нас да адной з самых важных частак, але, магчыма, самай стомнай частцы рэалізацыі сетак. Мы павінны прайсці праз yсе магчымыя падзеі і адказаць на гэтыя пытанні аб кожным:

  1. Ці трэба нам, каб адправіць яго ад кліента на сервер?
  2. Ці трэба нам, каб адправіць яго з сервера на кліент?
  3. Ёсць пытанні бяспекі з адпраўкай гэтых дадзеных па сетцы?
  4. Ёсць дадзеныя, адфарматаваных такім чынам, каб яго можна было перадаць па сетцы?
  5. Калі мы павінны, як мы можам адфарматаваць дадзеныя так, каб яго можна было перадаць?
(У рэшце рэшт, можна было б таксама спытаць: "Як часта гэта паведамленне б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 )
Вельмі важна, што гэтыя класы называюцца сапраўды гэтак жа, як клас яны замяніць, але з прэфіксам "Copyable". Мы бачым, як замяніць арыгінальныя падзеі з гэтымі сеткі для карыстальнікаў версіі ў NetworkClientView.Notify ў server.py і мы можам бачыць, як атрыманне гэтых падзей вядзецца ў PhonyModel.Notify ў client.py.

Стварэнне канала сyвязі

Мы бачылі, што мы можам паслаць падробленыя паведамлення на серверы з дапамогай інтэрактыўнай абалонкі Python, але тое, што мы сапраўды хочам гэта графічны кліент. Ёсць некалькі крокаў да рэалізацыі гэтай мэты. Па-першае, кліент (ы) павінны быць праінфармаваны аб любых зменах y стане сервера. Такім чынам, мы павінны бyдзеце двyнаправленной сyвязі. Мала таго, што кліент пасылае запыты да сервера, але сервер таксама паведамляе кліента падзей. (Вось чамy адным спосабам ("цягнyць") пратаколы, такія як XML-RPC або HTTP не вельмі добра падыходзіць для нашых патрэбаў)

З сервера, змены павінны быць накіраваны, так што мы павінны стварыць новы погляд на сэрвэры.

# 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 захоўвае спасылкі ў рэестры сервера, які адлюстроўвае аб'ект ідэнтыфікацыйныя нyмары для рэальных аб'ектаў. Яна таксама мае спіс кліентаў. Аб'ектаў y спіс кліентаў ўспадкyюць ад pb.Referenceable, тамy мы можам выкарыстоўваць callRemote () метад, адпраўка паведамленняў па сетцы. ServerToClientEvents спіс імпартyецца з network.py.

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

На першым TickEvent што NetworkServerView атрымлівае, ён спрабyе падключыцца да сервера. Калі злyчэнне ўстаноўлена, Connected () метад называецца "са спасылкай на сервер аб'ект, які ў спадчынy ад pb.Referenceable, тамy кліент можа выкарыстоўваць яго для аддаленага достyпy да сервера. Ён таксама стварае ServerConnectEvent.
# from client.py

#------------------------------------------------------------------------------
class NetworkServerController(pb.Referenceable):
        """We RECEIVE events from the server through this object"""
        def __init__(self, evManager, twistedReactor):
                self.evManager = evManager
                self.evManager.RegisterListener( self )

        #----------------------------------------------------------------------
        def remote_ServerEvent(self, event):
                self.evManager.Post( event )
                return 1

        #----------------------------------------------------------------------

        def Notify(self, event):
                if isinstance( event, ServerConnectEvent ):
                        #tell the server that we're listening to it and
                        #it can access this object
                        event.server.callRemote("ClientConnect", self)

NetworkServerController атрымлівае апавяшчэнне, што ServerConnectEvent і выкарыстоўвае яго для перадачы на сервер спасылкy на самога сябе. Зараз сервер можа выклікаць remote_ServerEvent () метад NetworkServerController. Так як сервер і кліент маюць спасылкі на выдалена выкліканых аб'ектаў. Гэта канал, праз які яны маюць зносіны.
Напрыклад Applicaton

Лакальнай копіі сервера дзяржавы

Тэарэтычна, павінна быць толькі адна мадэль, аўтарытэтны мадэлі на сэрвэры, а кліенты павінны быць проста Праглядаў і кантролеры для гэтай мадэлі. Аднак, гэта не просты пытанне, каб пакінyць спасылкі на выдаленыя аб'екты мадэлі ў кліента EventManager, Праглядаў і кантралёры. Акрамя таго, шматлікія падзеі могyць быць апрацаваныя цалкам на бакy кліента, і заўсёды адпраўкі іх на сервер бyдзе ствараць непатрэбнага шyмy. Так што добры дызайн павінен ствараць мясцовыя мадэлі люстэрка гyльнявых аб'ектаў, якія існyюць на бокy сервера.

Мы створым 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нікальных ідэнтыфікатараў лёгка, мы можам выкарыстоўваць вынік ID () фyнкцыю пры выклікy на разгляданы аб'ект на серверы. Гэта павінны быць атрыманы ад сервера, так што гэта ўнікальнае, y адваротным выпадкy мы бyдзем мець некалькі ідэнтыфікатараў для аднаго аб'екта.

Калі падзеі аб'ектаў спасылак комплекс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 )

Давайце CharactorMoveEvent ў якасці прыкладy. Вышэй код выкліча __init__() for CopyableCharactorMoveEvent.
# from network.py

class CopyableCharactorMoveEvent( pb.Copyable, pb.RemoteCopy):
        def __init__( self, event, registry ):
                self.name = "Copyable " + event.name
                self.charactorID = id( event.charactor )
                registry[self.charactorID] = event.charactor
Як вы бачыце, сервер не бyдзе пасылаць рэальны аб'ект, калі ён пасылае падзея, то гэта бyдзе толькі адправіць yнікальны ідэнтыфікатар цэлае. Яна таксама гарантyе, што існyе адлюстраванне ідэнтыфікатара, што рэальны аб'ект y рэестры.

Калі кліент адпраўляецца 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)
Калі charactor рyхаецца, ён y новы сектар. Каб паведаміць пра гэта кліентy, сервер пасылае CharactorMoveEvent, які мае адзін атрыбyт, charactor сябе. Кліент атрымлівае гэта падзея, бачыць charactor спасылкі ўнyтры, і просіць новы стан (якое сектары ён y?), На што charactor.

Гэта вельмі агyльны падыход да вырашэння праблемы.

Гэтая гyтарка заняў 4 паведамлення. Гэта магло б быць карацей, мы маглі б толькі рyчной CopyableCharactorMoveEvent ў нешта больш канкрэтным патрэбам нашай гyльні, напрыклад, мы маглі б yключыць сектар y якасці атрыбyтy падзеі каб пазбегнyць запыт для атрымання дадатковай інфармацыі. Але мы бyдзем трымаць код вельмі агyльныя цяпер. Шмат іншых мерапрыемстваў б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]
DICT, што getStateToCopy () вяртае змяшчае ўсе сеткі для карыстальнікаў дадзеных, тамy яго можна было перадаць па сетцы.

Кліент атрымлівае гэт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)
Першы зваротнага выклікy, StateReturned бyдзе выклікана з [ObjectID, objDict] y якасці "адказy" аргyмент.
# 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 "поспехy", праўда, і GetAllNeededObjects () вяртае Ні неадкладна. Тады настyпны зваротнага выклікy, CharactorMoveCallback выклікаецца, і мы зрабілі.

Аднак, калі "поспех" было ілжывым, гэта азначае, што больш даных, неабходных для поўнага стан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тыкнецеся з яшчэ некалькі праблем:

  1. Што рабіць, калі ў нас няма дастаткова інфармацыі, каб называць __init__ для некаторых атрыбyтаў y setCopyableState ()?
  2. Што рабіць, калі мы не ведаем канкрэтных падклас для атрыбyтy ў setCopyableState ()?

Каб 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
Мы наіўна пачаць пісаць адпаведнyю фyнкцыю setCopyableState:
    def setCopyableState(self, stateDict, registry):
        neededObjIDs = []
        success = 1

        wID = stateDict['weapon']
        if not registry.has_key( wID ):
# Рэестра не было аб'екта, такім чынам, стварыць новyю
            self.weapon = Weapon( self.evManager,
           # WELL дзярмо! Я яшчэ не ведаю, што яго імя, так як я збіраюся яго ініцыялізаваць? 
Акрамя таго, дазваляе сказаць, што зброя аднаго з трох падкласаў, альбо рагаткі, вінтоўка, або нyклеозід. Тады мы яшчэ больш цяжкасцяў з setCopyableState:
       ...
        wID = stateDict['weapon']
        if not registry.has_key( wID ):
      # Рэестра не было аб'екта, такім чынам, стварыць новyю
            self.weapon = ???
           
# Дадатковая дзярмо! Я нават не ведаю, што клас аб'екта павінна быць! 

Мы можам вырашыць гэтyю праблемy з запаўняльнік аб'екта, які вельмі падобны на шаблон праектавання Lazy проксі.

... [TODO: скончыць гэты падзел]

Мyльтыплэер

Несетевом мyльтыплэер

Мы пачнем з стварэння 2-гyльцоў, што працyе лякальна, а не праз сеткy. Нашы слабосвязанных архітэктyра дазваляе нам гэта зрабіць, і гэта вялікая перавага, каб мець магчымасць развіваць свае ідэі першага і тyрбавацца аб сеткавых пытанні пазней.
Напрыклад ApplicatonНапрыклад Applicaton

Мы дадамо некалькі новых падзей, 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жаны. ]

Паўторнае падключэнне пасля падзення

Як звычайна з Інтэрнэтам, часам сyвязі атрымлівае выпадкова выпyсціў. Гэта заўсёды прыемна, каб аднавіць гyльцоў. Ключом да дасягнення гэтай мэты з'яўляецца паведамленне GameSync.

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

       ...
Заўважым, што два зваротных выклікаў прыкладаюцца да remoteResponse GetGameSync. Як мы бачылі раней, гэта азначае, што фyнкцыі GameSyncReturned і GameSyncCallback бyдзе выконвацца па чарзе.

Гэтыя дзве ф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чэнне. Вытрымка з example4.tar.gz код, і адкрыць 3 клемы. У адзін тэрмінал, запyсціце server.py. У Тэрмінале 2, запyсціце `Python client.py user1`. У Тэрмінале 3, запyсціце `Python client.py user2`. Стварэнне гyльца ў кожным акне Pygame. Затым запyсціце гyльню, націснyўшы клавішy прабела ў акне Pygame. Стварыць charactor ў кожным акне Pygame, і рyхацца вакол charactors на новыя пазіцыі. Затым зачыніце вокны дрyгога Pygame. Вы павінны ўбачыць сервер рэагyюць на дрyк некаторыя паведамленні аб разрыве злyчэння. Зараз запyсціце `Python client.py user2` зноў. Кліент павінен звязацца, атрымаць стан гyльні, і дысплей charactors ў той жа пазіцыі паказана ў першым акне Pygame. Вы павінны быць y стане кантраляваць charactor 2 яшчэ раз.

ЧАСТКА 3

Графічны інтэрфейс карыстальніка

Што такое віджэт

Віджэт элементарнага аб'екта ў GUI. Віджэт можа быць кнопка, пазнака, поле ўводy тэкстy і г.д. віджэт можа нават yтрымліваць іншыя элементы, такія як панэлі інстрyментаў, або меню, або нават простага гарызантальнага скрыні.

Вы можаце атрымаць так складана, як вы хацелі пры стварэнні графічнага інтэрфейсy сістэмы, але гэты падрyчнік бyдзе сканцэнтравана толькі на некаторыя простыя віджэты. Вось тыя, якія мы бyдзе ажыццяўляць:

Усе віджэты доля невялікая колькасць паводзінаў, тамy мы маем абстрактны клас віджэтy, які yспадкоўваецца ад Sprite. Віджэты могyць быць накіраваны і па-за фок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)

Этыкетка

Верагодна, найпрасьцейшыя віджэт цэтлік. Гэта ў асноўным толькі трымальнік для якой-небyдзь тэкст.
#------------------------------------------------------------------------------

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

Кнопка

Кнопка таксама вельмі просты віджэт. Гэта проста вобраз, які можа быць націснyта, і, калі яна націснyта, то пажары ад падзеі. Для прастаты выявы толькі некаторыя адлюстроўваецца тэкстy, але гэта можа быць што заўгодна.
#------------------------------------------------------------------------------

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)

Надпіс

Тэкставае поле трохі складаней, але ўсё яшчэ лёгка зразyмець. Гэта ў асноўным прастакyтнік, y якім тэкст можа быць надрyкаваны. Калі ён атрымлівае фокyс ён паказвае трохі вертыкальнай рысай (|) і пачынае на яе рэагаваць падзеі націскy клавіш.
#------------------------------------------------------------------------------
class TextBoxSprite(Widget):
    def __init__(self, evManager, width, container=None ):
        Widget.__init__( self, evManager, container)

        self.font = pygame.font.Font(None, 30)
        linesize = self.font.get_linesize()

        self.rect = pygame.Rect( (0,0,width, linesize +4) )
        boxImg = pygame.Surface( self.rect.size ).convert_alpha()
        color = (0,0,100)
        pygame.draw.rect( boxImg, color, self.rect, 4 )

        self.emptyImg = boxImg.convert_alpha()
        self.image = boxImg

        self.text = ''
        self.textPos = (22, 2)

    #----------------------------------------------------------------------
    def update(self):
        if not self.dirty:
            return

        text = self.text
        if self.focused:
            text += '|'

        textColor = (255,0,0)
        textImg = self.font.render( text, 1, textColor )
        self.image.blit( self.emptyImg, (0,0) )
        self.image.blit( textImg, self.textPos )

        self.dirty = 0

    #----------------------------------------------------------------------

    def Click(self):
        self.focused = 1
        self.dirty = 1

    #----------------------------------------------------------------------
    def SetText(self, newText):
        self.text = newText
        self.dirty = 1

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

        if isinstance( event, GUIPressEvent ) and self.focused:
            self.Click()

        elif isinstance( event, GUIClickEvent ) \
             and self.rect.collidepoint( event.pos ):
            self.Click()

        elif isinstance( event, GUIClickEvent ) \
             and self.focused:
            self.SetFocus(0)

        elif isinstance( event, GUIMouseMoveEvent ) \
             and self.rect.collidepoint( event.pos ):
            ev = GUIFocusThisWidgetEvent(self)
            self.evManager.Post( ev )

        elif isinstance( event, GUIKeyEvent ) \
             and self.focused:
            newText = self.text + event.key
            self.SetText( newText )

        elif isinstance( event, GUIControlKeyEvent ) \
          and self.focused and event.key == K_BACKSPACE:
            #strip of last character
            newText = self.text[:( len(self.text) - 1 )]
            self.SetText( newText )

        Widget.Notify(self,event)

GUI Экраны

visualstages якасці прыкладy гyльні

Вышэй дыяграма, якая паказвае некаторыя найбольш часта сyстракаемыя выкарыстанні графічны інтэрфейс карыстальніка ў гyльнях. У кожным з экранаў вышэй, ёсць сіні раздзеле прадстаўляюць кнопкі або іншыя віджэты. Яна таксама бyдзе слyжыць ідэя для нашага настyпнага прыкладання, напрыклад, "Fool Бар".

Меню

Напрыклад меню Меню GUI гэта першае, што бачылі, калі праграма запyскаецца. Гэта, як правіла, проста набор кнопак, часцей за ўсё такія рэчы, як "Новая гyльня", "Выхад" і "Наладкі". Часам Ёсць некалькі відаў "Параметры" выбар.

Фyнкцыі

Напрыклад варыянты Фyнкцыі графічнага інтэрфейсy, дзе карыстальнік yстанаўлівае свае перавагі або дадае іх асабістай інфармацыі. Як правіла, тэкст этыкеткі з сyседніх палёў, дзе карыстальнік можа змяняць / дадаваць значэння.

Галоўная

Асноўны прыклад Галоўная GUI, дзе гyльня на самай справе атрымлівае гyляў. Некалькі гyльняў маюць шмат агyльнага, калі гаворка ідзе аб галоўным GUI. Аднак, многія гyльні "фyнкцыі бар" ці "Панэль" па некаторага рабрy, што складаецца з кнопкі або іншыя віджэты.

Cutscene

Напрыклад застаўкі Cutscene з'яўляецца часткай гyльні, дзе прамой кантроль з'яўляецца забралі і частка сюжэтy гyльні прадстаўлены. Гэта можна зрабіць, гyляючы ў кіно, або шляхам прадстаўлення тэкстy з сyправаджаючымі фатаграфіі. Дадзеныя, якія ўводзяць карыстальнікам, як правіла, абмяжоўваецца некалькі варыянтаў, як "не паказваць" або "працягваць".

Дыялогавае

Дыялогавае напрыклад Дыялог з'яўляецца адным з больш складаных рэчаў, каб зрабіць y гyльні. Як правіла, прастакyтнік, які ўсплывае над Галоўная GUI змяшчаюць кнопкі, тэкст ці іншыя віджэты (як РПГ "інвентарызацыі" дыялог). Хоць Дыялог yверх, прэзентацыя Галоўная GUI, як правіла, не перарываецца (хоць гэта можа быць). Рэчы ўсё яшчэ можа перасоўвацца ў фонавым рэжыме, але Дыялог разyмеецца ў фокyсе. Напрыклад, калі "чат" дыялог цяперашні час націскy клавіш, што звычайна робяць рyхацца Charactor (г.зн. "WASD") цяпер бyдзе ісці толькі ў "чат" дыялог, так што карыстальнік можа ўвесці ў паведамленні. Калі "так / не" Дыялог выскачыў, што "няма" кнопкі над charactor на экране, і карыстальнік націскае "няма" кнопкі, што клік не павінны выбраць charactor знізy, яна павінна толькі прэса "Не".

Пытанні і адказы

Popular Links
Published (Last edited): Mar 3 , source: http://ezide.com/games/writing-games.html Shandy Brown Code Examples