Sjbrown Písania, Hry Tutoriál

link: http://ezide.com/games/writing-games.html

Shandy Brown.

Čo By Ste mali Vedieť

Táto príručka predpokladá určitú úroveň vedomia. Ak zistíte, že to môže byt ‘ mätúce, buď by ste mali štetcom na niektoré z týchto pojmov, alebo by som mal stať lepším programátorom. Je to druh nás stavia do preteky v zbrojení sú leniví.

Objektovo-Orientované Programovanie

Očakáva sa, že čitateľ je pohodlné v objektovo-orientované prostredie. Všetky dôležité kód je štruktúrovaná s triedami.

Návrhových Vzorov

Návrhových Vzorov je komunikačný nástroj a nemajú diktovať dizajn, informujú čítanie kódu. Táto príručka využíva Design Patterns “Model View Controller” (MVC), “Mediátora”, a “Lazy sa server Proxy”. Čas nebude strávil popisujúcich tieto vzory podrobne, takže ak sa zvuk zahraničných vám odporúčam kontrolu z knihy “Design Patterns” Gamma et al. alebo len surfovanie na webe návody.

ČASŤ 1

Príklad Cieľom

Je to vždy dobré načrtnúť svoju hru buď s obrázky alebo text skôr, než začnete kódovanie.

Začneme tým, že sa snažia vytvoriť program, kde malý muž sa pohybuje okolo mriežky z deviatich štvorcov. Toto je príliš jednoduchý príklad, ale ľahko rozšíriteľný nechceme sa dostať viazaný na pravidlá hry, namiesto toho, aby sme sa mohli zamerať na štruktúru kódu.

príklad aplikačných

Architektúry

Model View Controller

Výber MVC by malo byť dosť jasné, kde grafické hra je dotknuté. Primárny Model sa bude diskutovať, neskôr pod názvom Hry Modelu. Primárny Pohľad bude PyGame okno zobrazenie grafiky na monitore. Primárny Radič bude klávesnice, podporuje PyGame interného pygame.podujatie modul.

Nemáme dokonca dostal do Modelu ešte, a už tu máme ťažkosti. Ak ste oboznámení s pomocou PyGame, pravdepodobne ste zvyknutí na hlavnej slučke, ako je tento:

 # ukradnuté z príkladu ChimpLineByLine na pygame.org 
 main (): 
    ... 
    zatiaľ čo 1: #Udalosti zadávania vstupnej udalosti 
        pre udalosť v pygame.event.get (): 
            if event.type == QUIT: 
                return 
            elif event.type == MOUSEBUTTONDOWN: 
                fist.punch () 
            elif event.type == MOUSEBUTTONUP: 
                fist.unpunch () #Draw Všetko 
        allsprites.update () 
        screen.blit (pozadie, (0, 0)) 
        allsprites.draw (obrazovka) 
        pygame.display. otočiť ()

V tomto príklade je riadiaca jednotka (časť “Udalosti manipulácie s účtami”) a zobrazenie (časť “Draw Everything”) sú pevne spojené a to je všeobecne ako fungujú hry PyGame, pri každej iterácii hlavnej slučky sa očakáva že budeme kontrolovať vstupné udalosti, aktualizovať všetky viditeľné sprites a preklopiť obrazovku. Skúsenosť nám hovorí, že s rastúcim kódom bude táto časť ochabnutá. Usporiadaním tohto vzoru v MVC oddeľujeme zobrazenie a kontrolér. Naším riešením je predstaviť funkciu Tick (), ktorú neustále opakujúca hlavná slučka môže volat pre zobrazenie aj riadenie. Takýmto spôsobom nebude k dispozícii kód špecifický pre zobrazenie na rovnakom mieste ako kód špecifický pre riadiaceho počítača. Tu je hrubý príklad:

ControllerTick ():
     #Udalosti vstupnej udalosti 
    pre udalosť v pygame.event.get (): 
        if event.type == QUIT: 
            návrat False 
        elif event.type == MOUSEBUTTONDOWN: 
            fist.punch () 
        elif event.type == MOUSEBUTTONUP: 
            fist.unpunch () 
    návrat True 

 ViewTick (): #Draw Všetko 
    ... 
 main (): 
    ... 
    zatiaľ čo 1: 
        ak nie ControllerTick (): 
            vrátiť 
        ViewTick ()
    

Tu je viac informácií o vzore MVC : MVC @ Wikipedia MVC @ ootips.org

Zdôvodnenie

Čitatelia s určitými skúsenosťami s písaním hier môžu byť v tomto bode blázniví, keď myslí, že MVC je zložitejšia, než je potrebné, a že to pridá nepotrebné režijné náklady, najmä ak je cieľom vytvoriť jednoduchú, arkádovú hru. Teraz historicky arkádové hry boli práve tie, hry napísané pre arkádové stroje. Kód sa rozbehol “blízko k kovu” a vytlačil všetky zdroje zariadenia, aby získal 3-farebný duch, ktorý bude blikať modrým každým ďalším rámcom. V 21. storočí máme osobné počítače s bohatými zdrojmi, kde aplikácie prebiehajú pár vrstiev nad kovom. Preto organizovanie kódu do vzoru má malé relatívne náklady. Za tieto malé náklady získate nasledujúce výhody: jednoduchšie pridávať siete, ľahko pridávať nové pohľady (loggery súborov, radary, HUDy, viacnásobné úrovne priblíženia …), ponechajte kód modelu ”

Prostredník

Poďme preskúmať nekonečné zatiaľ čo slučka v poslednom kuse kódu. Aká je jej práca? V podstate posiela hlásenie Tick () do zobrazenia a ovládacieho prvku tak rýchlo, ako to spravuje procesor. V tomto zmysle je možné ho vnímať ako hardvér, ktorý posiela správy do programu, rovnako ako klávesnica; môže sa považovať za iného kontrolóra.

Možno, že ak čas “stenových hodín” ovplyvní našu hru, bude ešte ďalší riadiaci systém, ktorý posiela správy každú sekundu, alebo možno bude ďalší pohľad, ktorý vypíše text do súboru denníka. Teraz musíme zvážiť, ako sa budeme zaoberať viacerými zobrazeniami a kontrolórmi. To nás vedie k ďalšiemu modelu v našej architektúre, mediátorovi.

architektúra

Implementujeme vzor mediátora vytvorením objektu EventManager. Tento sprostredkovateľ umožní viacerým poslucháčom oznámiť, keď sa zmení niektoré iné zmeny objektov. Navyše tento meniaci sa objekt nemusí vedieť, koľko poslucháčov je, môže byť dokonca pridaných a odstránených dynamicky. Všetok meniaci sa objekt musí robiť poslať udalosť EventManager, keď sa zmení.

Ak chce objekt počúvať udalosti, musí sa najprv zaregistrovať v aplikácii EventManager. Použijeme slabý odkaz WeakKeyDictionary, aby sa poslucháči nemuseli explicitne registrovať. [TODO: oveľa slabšie odôvodnenie. gc, atď]

Tiež vytvoríme triedu udalostí, ktorá zapracuje udalosti, ktoré sa dajú odoslať cez EventManager.

Trieda
    udalosti: "" "To je supertrieda k žiadnej udalosti, ktoré by mohli byť generované s 
    objektom a odoslaných na EventManager 
    , '" 
    def __init __ (seba): 
        self.name = "Generic Event" 

triedny EventManager: "" "tento objekt je zodpovedný za koordináciu väčšiny komunikácie 
    medzi modelom, zobrazením a kontrolórom. 
    "" " 
    def __init __ (vlastné): 
        od slabého importu WeakKeyDictionary 
        self.listeners = WeakKeyDictionary () # -------------- -------------------------------------------------- ------ 
    def RegisterListener ( 
        vlastník , poslucháč): self.listeners [listener] = 1
    

    

    # ------------------------------------------------- --------------------- 
    def UnregisterListener ( 
        vlastník , poslucháč): ak poslucháč v self.listeners.keys (): 
            del self.listeners [listener] # - -------------------------------------------------- ------------------ 
    def Post (vlastné, udalosť): "" "Pošlite novú udalosť, ktorá bude vysielaná všetkým poslucháčom" " 
        pre poslucháča v self.listeners .keys (): #NOTE: Ak slabý signál zomrel, bude #automaticky odstránený, takže nemáme žiadne obavy. 
            listener.Notify (udalosť)

Tu je hrubý nápad, ako by to mohlo byť integrované s predchádzajúcim kódom.

Trieda KeyboardController: 
    ... 
    def Notify (vlastné, udalosť): 
        if is (event, TickEvent): # Event Handle Events 
            ... 
trieda CPUSpinnerController: 
    ... 
    def Run (vlastné): 
        while self.keepGoing: 
            event = TickEvent ) 
            self.evManager.Post (event) 
    def Informovať (self, event): 
        ak isinstance (event, QuitEvent): 
            self.keepGoing = False 
            ... 
class PygameView: 
    ... 
    def Informovať (self, event): 
        ak isinstance ( udalosť, TickEvent): #Draw Všetko 
            ... 
 main ():
            




            

    ... 
    evManager = EventManager () 

    kláv = KeyboardController () 
    kužeľ = CPUSpinnerController () 
    pygameView = PygameView () 
    
    evManager.RegisterListener (kláv) 
    evManager.RegisterListener (spinner) 
    evManager.RegisterListener (pygameView) 

    spinner.Run ()

Odchýlka: Typy udalostí a selektívne poslucháči

Ako stále viac a viac poslucháčov možno nájdeme, že je neúčinné spamovať každého poslucháča pri každej udalosti. Možno, že niektorí poslucháči sa starajú len o určité udalosti. Jedným zo spôsobov, ako zefektívniť veci, je klasifikovať udalosti do rôznych skupín.Na účely tejto príručky budeme používať iba jeden druh podujatia, takže každý poslucháč dostane spam s každou udalosťou.

Pokročilí manažéri udalostí

Ak sa pokúsite použiť túto konkrétnu triedu Event Manager pre svoj vlastný projekt, možno si všimnete, že má nejaké nedostatky. Konkrétne, ak blok kódu generuje sekvencie A a B postupne a poslucháč zachyti udalosť A a generuje udalosť C, vyššie uvedená trieda Event Manager spracuje udalosti v poradí A, C, B namiesto požadovaného poradia A, B, C. V neskorších príkladoch uvidíte príklad pokročilejšieho manažéra udalostí, ktorý vždy poskytuje udalosti v požadovanom poradí.

Tu sú niektoré ďalšie informácie o vzore Mediator a súvisiacom vzore pozorovateľa: Mediator @ Wikipedia Observer @ ootips.org

Model hry

Pri vytváraní modelu musíme prejsť procesom nazvaným “abstrakcia”. Hráli sme už predtým, takže máme cennú duševnú knižnicu konkrétnych príkladov hotových výrobkov, ktoré sú v podstate podobné ako hotový výrobok, ktorý chceme vytvoriť. Ak môžeme nájsť v týchto konkrétnych príkladoch abstraktné spoločné prvky, pomôže nám to vytvárať kurzy na usporiadanie nášho kódu, jeho flexibilitu, udržateľnosť a poskytnutie slovnej zásoby, aby sme mohli hovoriť s ostatnými členmi tímu o kódexe. Existuje veľa možných abstrakcií, s ktorými môžeme dospieť a posúdenie, či sme vytvorili dobrú abstrakciu, je veľmi subjektívne. Je dôležité mať na pamäti svoje ciele a tiež predvídať, ako sa môžu požiadavky v budúcnosti zmeniť.

Tu je model, ktorý pre mňa pracoval a je dostatočne všeobecný na prispôsobenie sa mnohým typom hier:

Príklad aplikácie

Hra

Hra je hlavne objekt kontajnera. Obsahuje Prehrávače a Mapy. Môže tiež robiť veci ako Štart () a Dokončiť () a sledovať, kto je.

prehrávač

Objekt prehrávača predstavuje skutočný človek (alebo počítač), ktorý hrá hru. Bežné atribúty sú Player.score a Player.color. Nezamieňajte ho s Charactorom. Pac Man je Charactor, osoba držiaca joystick je hráč.

charactor

Charactor je niečo, čo ovláda hráč, ktorý sa pohybuje po mape. Synonymá môžu byť “Jednotka” alebo “Avatar”. Je úmyselne napísané “Charactor”, aby sa zabránilo nejednoznačnosti s znakom, ktorý môže znamenať aj “jediný list” (tiež nemôžete vytvoriť tabuľku v PostgreSQL s názvom “Character”). Bežné atribúty Charactor sú Charactor.health a Charactor.speed.

V našom príklade bude “malý muž” náš jediný Charactor.

mapa

Mapa je oblasť, v ktorej sa Charactors môžu pohybovať. Existujú zvyčajne dva typy máp, oddelené tie, ktoré majú Sektory a súvislé mapy s umiestneniami. Šachovnica je príkladom diskrétnej mapy. Trojrozmerná úroveň v programe Quake (s presnosťou na plávajúcom bode) alebo úroveň v programe Super Mario (s presnosťou pixelov) sú príkladmi nepretržitých máp.

V našom príklade mapa bude diskrétnou mapou s jednoduchým zoznamom deviatich sektorov.

sektor

Sektor je súčasťou mapy. Nachádza sa v blízkosti iných oblastí mapy a môže mať zoznam takýchto susedov. Žiadny Charactor nemôže byť logicky medzi sektormi . Ak je Charactor v sektore, je to v tomto sektore úplne a nie v žiadnom inom sektore (hovorím tu funkčne. Môže to vyzerať, ako by to bolo medzi sektormi, ale to je problém pre názor, nie pre Model)

V našom príklade neumožníme žiadne diagonálne pohyby, iba hore, dole, doľava a doprava. Každý povolený krok bude definovaný zoznamom susedov pre konkrétny sektor, pričom stredný sektor má všetky štyri.

umiestnenia

Nebudeme sa dostať do lokalít nepretržitej mapy, pretože sa nevzťahujú na náš príklad.

položka

Všimnete si, že na obrázku nie je položka explicitne spojená s ničím. To je ponechané na vývojárovi. Mohli by ste mať obmedzenie konštrukcie, že položky musia byť obsiahnuté v Charactoroch (možno v prostrednom objekte “Inventory”), alebo je možné, že vaša hra má väčší zmysel zostať v zozname veľa objektov v hre. Niektoré hry si môžu vyžiadať sekcie s položkami, ktoré ležia v ich vnútri.

Náš príklad

Príklad aplikácieTento príklad využíva všetko, čo sa doteraz zaoberá. Začína so zoznamom prípadných udalostí, potom definujeme nášho sprostredkovateľa EventManager so všetkými metódami, ktoré sme predtým ukázali.

Ďalej máme naše ovládače, KeyboardController a CPUSpinnerController. Všimnete si, že stlačenie klávesov už nie je priamo riadené niektorým objektom hry, ale len generuje udalosti, ktoré sú odosielané do aplikácie EventManager. Tak sme oddelili ovládač od modelu.

Ďalej máme časti nášho programu PyGame View, SectorSprite, CharactorSprite a PygameView. Všimnite si, že SectorSprite vedie odkaz na sektorový objekt, ktorý je súčasťou nášho modelu. Nechceme však pristupovať k žiadnym metódam tohto sektorového objektu priamo, my ho len používame na identifikáciu sektoru, ktorý objekt SectorSprite zodpovedá. Ak chceme, aby bolo toto obmedzenie explicitnejšie, mohli by sme použiť funkciu id ().

Pygame View má skupinu pozadia zelených námestiek, ktoré reprezentujú Sektorové objekty a skupinu popredia obsahujúcu nášho “malého človeka” alebo “červeného bodu”. Aktualizuje sa na každom TickEvent.

Nakoniec máme objektové objekty, ako je uvedené vyššie, a nakoniec funkciu main ().

Tu je schéma najdôležitejších prichádzajúcich a odchádzajúcich udalostí.

napríklad prichádzajúce správy napríklad odchádzajúce správy

ČASŤ 2

Internet Play

Našou ďalšou úlohou bude urobiť hru prehrávaná cez internet. Nakoniec to povedie k multiplayerovým schopnostiam v našej hre, ale je dôležité, aby sme urobili prvý krok pre jedného hráča, pretože nás vystavuje niekoľkým obmedzeniam, ktoré môžu ovplyvniť akýkoľvek budúci kód.

Kód v nasledujúcich sekciách je napísaný postupne, takže neočakávame, že jednoducho odoberieme kód z prvej časti a napíšeme s ním hru. Nasledujúce sekcie niekedy riešia problémy s predtým zobrazeným kódom a vysvetľujú, ako tieto problémy prekonať.

Rýchly vývoj

Jedným z cieľov tohto tutoriálu je ukázať, ako sa môže rýchlo rozvíjať herný program . Zvyčajne je čokoľvek, čo zahŕňa sieťovanie, anathema k “rýchlemu”, pretože akonáhle uvediete do siete, uvádzate multiprocessing, latenciu, manipuláciu s chybou a všeobecné vyťahovanie vlasov. Princíp príkladov kódu v tomto tutoriále je zabezpečiť, aby sa hra mohla spustiť bez toho, aby ste dokonca zapli sieť. Funkcia vytvárania sietí by nemala mať nijaký vplyv na kód v example.py – spustenie hry v režime pre jedného hráča by nemalo vykonávať žiadne cesty súvisiace so sieťami. Tým, že budeme prísne o tomto oddelení, dúfame, že sa nám podarí rýchlo rozvinúť hru , bez ohľadu na to, čo môže sieťový kód priniesť.

rôzne spôsoby štruktúry hostiteľov siete

Štruktúra hostiteľov siete

Počítačové procesy (zvyčajne existuje iba jeden proces záujmu na každom fyzickom počítači alebo “hostiteľovi”, takže používame len termín “hostiteľ”), ktoré komunikujú cez sieť, môžu byť organizované mnohými spôsobmi. V hrách existujú tri populárne spôsoby na štruktúrovanie hostiteľov, Peer-to-Peer, “Strict” Client-Server a “Servent” Client-Server. Rozhodujúcim faktorom pri rozhodovaní o štruktúre sieťových hostiteľov pre hry je zvyčajne dôvera. Hry sú emocionálne (dobre, dobré) a sú konkurencieschopné, hráči sú motivovaní k víťazstvu a keď nevyhrali, chcú veriť, že žiadny iný hráč nemá nespravodlivú výhodu. Na zabezpečenie dôvery je potrebný konzistentný autoritatívny model.

V štruktúre Klient-server “Strict” existuje jeden server “tretej strany”, ku ktorému sa pripoja všetci klienti. Každá zmena autoritatívneho modelu hry sa musí uskutočniť na serveri. Klient môže predpovedať autoritatívny stav, ale nesmie veriť do stavu hry, kým sa z servera nepočuje, že to je v skutočnosti prípad. Príkladom hry bude World of Warcraft.

Client-Server @ Wikipedia

V štruktúre klient-server “Servent” jeden z hráčov, zvyčajne ten, ktorý začína hru, pôsobí ako server. Toto trpí nevýhodou, že ostatní hráči dôverujú stavu hry tak, ako dôverujú tomu konkrétnemu hráčovi. Žiadna tretia strana však nie je potrebná. Príklady nájdete v mnohých prvých strieľačkách. Táto štruktúra je často spárovaná s serverom “zodpovedajúceho” tretej strany, ktorý spája hráčov medzi sebou a potom odovzdáva hostiteľovi služby Servent.

Servent @ Wikipedia

V štruktúre Peer to Peer majú všetci hostitelia rovnaké úlohy. Veľkou výhodou štruktúry Peer to Peer je to, že robustne sa zaoberá sieťovými odpojeniami od jednotlivých hostiteľov. Dôvera je ohrozená. Dôvera môže byť posilnená prijatím stratégie prechodu tokenu tak, že hostiteľ držiaci token pôsobí ako servent.

Peer to Peer @ Wikipedia

Pre naše príklady budeme skúmať štruktúru “Strict” Client-Server.

Synchrónne / asynchrónne

Keď zavoláte funkciu, ktorá vyžaduje sieťovú komunikáciu, môže to trvať dlhšie. Ak sme čakali na ukončenie funkcií závislých od siete predtým, než zavoláme funkcie, ktoré nakreslia grafiku, používatelia by sa naštvali a plamenili nás na internetových výveskách. Riešením je napísať funkcie, ktoré posielajú správy cez sieť a potom sa okamžite vrátia, nečakajúc na odpoveď. Odpovede budú nakoniec pochádzať od vzdialených hostiteľov a čakať vo fronte, kým ich náš proces nedokáže poskytnúť. Je dôležité mať na pamäti, že odpovede nemusia byť zaradené do rovnakého poradia ako požiadavky.

Táto asynchrónna kvalita je základom pre sieťový kód. Našťastie, keď navrhujeme náš kód tak, aby existoval nezávislý EventManager a dobre definované udalosti, spraví asynchrónne správy zo siete pomerne bezbolestné.

Tento návod používa kód Twisted pre kód súvisiaci so sieťou. Odporúčam vám čítať Twisted dokumentáciu, hoci by nemalo byť potrebné prejsť cez tento tutoriál. (všimnite si, že veľa Twisted dokumentácie sa zameriava na písanie serverov, kde nie je známa implementácia klienta.) Odporúčame preskočiť dopredu sekcie Perspektive Brokers.) Nápady, ktoré tu nájdete, by mali byť nezávislé od výberu Twisted; príklady by sa mohli rovnako dobre realizovať aj so surovými zásuvkami alebo nosnými holubmi.

Twisted je rámec, ktorý ukrýva frontu od nás, očakáva od programátora, že zavolá reaktor.run (), ktorý je hlavným panelom, ktorý spotrebováva frontu a spúšťa spätné volania. Spätné volania poskytuje programátor.

Uskutočnenie

Príkladový server

V prípade servera začneme s rovnakým kódom ako predtým. Jednoducho premenujte example.py na server.py.

Normálne server je niečo, čo beží ako démon alebo v textovej konzole; nemá grafické zobrazenie. Môžeme to jednoducho nahradiť tak, že PygameView nahradíme TextLogView takto:

# ------------------------------------------------- ----------------------------- 
trieda TextLogView: 
        "" "..." "" 
        def __init __ (self, evManager): 
                ja .evManager = evManager 
                self.evManager.RegisterListener (vlastné) # ------------------------------------- --------------------------------- 
        def Oznámiť (vlastné, udalosť): 
                ak je inštancia (udalosť, CharactorPlaceEvent): 
                        print event.name, "at", event.charactor.sector elif 
                isinstance (udalosť, CharactorMoveEvent): 
                        print event.name, "to", event.charactor.sektorová 
                elif nie je skutočnosť (udalosť TickEvent):
                                                                               
                                                                               
        
                                                                               
                                                                               
                                                                               
                        vytlačiť názov udalosti

Už teraz využívame výhody modelu MVC. Zmenou len malého množstva kódu už nemáme zobrazenie Pygame, namiesto toho TextLogView práve vytlačí prijaté udalosti do konzoly.

Ďalšia vec, ktorú nepotrebujeme na serveri, je vstup klávesnice, takže môžeme odstrániť KeyboardController. Odkiaľ prichádzajú vstupné správy? Pochádzajú zo siete, takže budeme potrebovať objekt Controller pre správy posielané klientmi, NetworkClientController.

zo skrútených.spread importu pb 
# ------------------------------------------- ----------------------------------- 
trieda NetworkClientController (pb.Root): 
        "" "..." "" 
        def __init __ (vlastné, evManager): 
                self.evManager = evManager 
                self.evManager.RegisterListener (self) 

        # -------------------------- -------------------------------------------- 
        def vzdialené_GameStartRequest (vlastné): 
                ev = GameStartRequest () 
                self.evManager.Post (ev) 
                návrat 1 

        # ---------------------------------- ------------------------------------ 
        def remote_CharactorMoveRequest (vlastné, smer):
                ev = CharactorMoveRequest (smer) 
                self.evManager.Post (ev) 
                návrat 1 

        # --------------------------------- ------------------------------------- 
        def Notify (vlastné, udalosť): 
                pass

Inštancia NetworkClientController je špeciálny objekt, ktorý sa dá posielať cez sieť prostredníctvom mechanizmu Twisted’s Perspective Broker (pretože zdedí z pb.Root). Vzdialený klient požiada o odkaz na inštanciu NetworkClientController, akonáhle ho prijme, môže zavolať ľubovoľnú metódu, ktorá začína na “remote_”. Takže pre klienta na odosielanie správ na server sme implementovali remote_GameStartRequest a remote_CharactorMoveRequest.

varovanie

Mohlo by byť lákavé, aby sa všetky objekty vzdialene pripomínali. (tj zdediť z pb.Referentable) Problém s týmto prístupom je, že pevne spája sieťový kód so zvyškom kódu. Je lepšie oddeliť sieťový kód tak, aby ostatné objekty používali stratégiu prechodu udalostí popísanú vzorom mediátora.V našich príkladoch budeme mať iba jednu triedu na serveri, ktorý je referenčný, a tiež iba jedna trieda v klientovi. [TODO: rozšíriť o toto]

CPUSpinnerController tiež nepotrebujeme na serveri, preto sme ho odstránili a nahradili ho reaktorom Twisted, ktorý podobne poskytuje metódu run ().

def main (): 
    evManager = EventManager () 

    log = TextLogView (evManager) 
    clientController = NetworkClientController (evManager) 
    game = Game (evManager) 
    
    z twisted.internet import reaktora 

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

    reactor.run ()

Predtým sme použili udalosť Tick na spustenie hry, teraz budeme musieť explicitne spustiť hru s novou udalosťou GameStartRequest.

trieda GameStartRequest (udalosť): 
        def __init __ (self): 
        self.name = "Game Start Request"

Nie je potrebné pochopiť Twisted časti tohto, môžete len považovať za “magické”. Čo by ste mali vedieť, je, že vyvolanie reaktora.run () spôsobí zablokovanie hlavného panelu pri počúvaní na portu 8000.

Ak budeme hrať nejaké špinavé triky, môžeme vidieť, čo náš server robí bez písania klienta. Namiesto toho sa k nemu pripojíme pomocou interaktívneho tlmočníka Pythonu. Teraz reaktor.run () je blokujúci hovor, ktorý sa nevráti, kým sa reaktor nevypne, takže aby sme sa vrátili k interaktívnemu výzve, musíme reaktor zničiť a potom zavolať reactor.iterate (), aby sme komunikovať s ním. Malo by byť samozrejmé, že to nie je odporúčaná prax. Ak replikujete reláciu nižšie, možno budete musieť volať iterate () niekoľkokrát predtým, než uvidíte nejaký výsledok.

$ python 
Python 2.5.2 (r252: 60911, Apr 21 2008, 11:17:30) 
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu7)] na linux2 
Typ "help", "copyright", "credits" "licencia" pre viac informácií. 
>>> z krouceného importu pb 
>>> z importovaného reaktora twisted.internet 
>>> factory = pb.PBClientFactory () 
>>> server = Žiadne 
>>> def dostalServer (serv): 
... globálny server 
.. 
>> server = server 
... 
>>> connection = reactor.connectTCP ('localhost', 8000, factory) 
>>> reaktor.callLater (4, reactor.crash) <inštancia twisted.internet.base.DelayedCall at 0xac5638 
>> >> reaktora.
<Odložené pri 0xb1f440 aktuálny výsledok: Žiadne> >> 
reaktor.iterate () 
>>> server.callRemote ('GameStartRequest') 
<Odložené na 0xac5638> 
>>> reaktor.iterate () 
>>> hore, left = 0,1,2,3 
>>> server.callRemote ('CharactorMoveRequest', up) 
<Odložené na 0xb1f4d0> 
>>> reaktor.iterate () 
>>> server.callRemote ('CharactorMoveRequest', vpravo) 
<Odložené at 0xac5638> 
>>> reaktor.iterate () 
>>> server.callRemote ('CharactorMoveRequest', nadol) 
<Odložené na 0xb1f4d0> 
>>> reaktor.iterate () 
>>> server.callRemote ('CharactorMoveRequest ', vľavo) 
<Odložené na 0xac5638> 
>>> reaktor.iterate ()

Príklad používania konzoly Python ako falošného klienta

$ Python server.py 
Game štart Dopyt 
Mapa dostaval závod 
Game začiatok udalosti 
charactor umiestnenie udalostí na <__ hlavnej __. Sektor inštanciu na 0xc9b290> 
charactor Move Request 
charactor Move Request 
charactor udalosť move to <__ hlavné __. Sektor inštanciu na 0xc9b320> 
charactor Move Request 
charactor Move Udalosť do <__ hlavnej __. Sektorová inštancia na 0xc9b290> 
Charactor Presunúť požiadavku 
Charactor Presunúť udalosť na <__ main __. Sektorová inštancia na 0xc9b3b0>

Spúšťa server.py Upozorňujeme, že žiadosť o posun hore neviedla k udalosti Pohyb.

Môžeme falošným klientom správnejším spôsobom pomocou nástroja Twisted, twisted.conch.stdio. Práve sme spustili pythonový tlmočník s týmto modulom a potom môžeme vynechať zneužitie reaktora:

$ Python -m twisted.conch.stdio 
>>> z twisted.spread dovozu pb 
>>> z twisted.internet dovoznej reaktora 
>>> 
>>> výroby = pb.PBClientFactory () 
>>> server = None 
>>> 
> >> def dostalServer (serv): 
... globálny server 
... server = serv 
... 
>>> pripojenie = reaktor.connectTCP ('localhost', 8000, továreň) 
>>> d = factory.getRootObject () 
> >> d.addCallback (gotServer) 
<Odložené pri 0xc227a0 aktuálny výsledok: Žiadne> 
>>> server.callRemote ('GameStartRequest') 
<Odložené # 0> 
Odložené # 0 volané späť: 1 
>>> hore,doprava, dole, doľava = 0,1,2,3 
>>> server.callRemote ('CharactorMoveRequest', hore)
<Odložené # 1> 
Odložené # 1 volané späť: 1 
>>> server.callRemote ('CharactorMoveRequest', vpravo) 
<Odložené # 2> 
Odložené # 2 volané späť: 1 
>>> server.callRemote 
<Odložené # 3> 
Odložené # 3 volané späť: 1 
>>> server.callRemote ('CharactorMoveRequest', vľavo) 
<Odložené # 4> 
Odložené # 4 volané späť: 1

Používanie stránky twisted.conch.stdio ako falošného klienta

Kráľ hradu

Ako je uvedené vyššie, objekt reaktora Twisted je navrhnutý tak, aby bol zodpovedný za hlavnú slučku. To vytvára určité problémy, pretože už máme hlavnú slučku v CPUSpinnerController. Mohli by sme podriaďovať Twistedov reaktor a “pumpovať” ho pri každej iterácii hlavnej slučky CPUSpinnerController, ale to má nevýhodu, že musíme zneužiť API Twisteda spôsobom, ktorý pravdepodobne nebol zamýšľaný a nemusí byť kompatibilný s dopredu.

# Príklad triedy, ktorá pumpuje 
triedu reaktora Twisted ReactorSlaveController (objekt): 
    def __init __ (self): 
        ... 
        factory = pb.PBClientFactory () 
        self.reactor = SelectReactor () 
        installReactor (self.reactor) 
        connection = self.reactor .connectTCP ('localhost', 8000, továreň) 
        self.reactor.startRunning () 
        ... 

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

    def Stop (vlastné): 
        self. reaktor.addSystemEventTrigger ("po", "vypnutie", 
                                            self.onReactorStop) 
        self.reactor.stop ()
        self.reactor.run () #externé čokoľvek zostáva v reaktore
 
    def onReactorStop (self): '' 'Toto sa nazýva keď je reaktor úplne dokončený' '' 
        self.rector = žiadny
        

Alternatívne môžeme Twisted použiť zamýšľaným spôsobom a potom použiť triedu LoopingCall, aby sme spustili udalosť Tick závislá od hlavnej slučky reaktora. Vytvorenie objektu LoopingCall je spôsob, akým chceme reaktor opakovane volat funkciu. Nevýhodou tohto prístupu je, že hry sa často začínajú v režime pre jedného hráča a nechceme vyvolať žiadny kód súvisiaci so sieťou, napríklad Twisted, ak si užívateľ nevyberie možnosť pre viacerých hráčov.

# Príklad použitia LoopingCall na oheň udalosti Tick 
z importu twisted.internet.task LoopingCall 

... 

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

loopingCall = LoopingCall (FireTick, evManager) 
interval = 1.0 / FRAMES_PER_SECOND 
loopingCall. štart (interval)

V konečnom dôsledku je voľba závislá od vás. Mali by ste zvážiť klady a zápory každého prístupu na základe typu hry, ktorú píšete. V príkladoch budeme využívať prístup čerpania reaktora.

Správy cez kábel

Predchádzajúci príklad servera priniesol dobrý prehľad o základnej technike sietí, ale je to trochu príliš jednoduché pre naše účely. Naozaj nechceme napísať novú funkciu pre každú správu, ktorú môže server získať. Namiesto toho by sme radi využili naše už existujúce triedy udalostí.

To nás privádza k jednej z najdôležitejších častí, ale možno aj k najväčšej únavnej časti implementácie sietí. Musíme prejsť všetkými možnými udalosťami a odpovedať na tieto otázky o každom z nich:

  1. Musíme ho poslať od klienta na server?
  2. Potrebujeme ho odoslať zo servera klientovi?
  3. Existujú problémy s bezpečnosťou pri odosielaní týchto údajov cez sieť?
  4. Sú dáta formátované takým spôsobom, aby mohli byť odoslané cez sieť?
  5. Ak je to potrebné, ako preformátujeme údaje tak, aby ich bolo možné odoslať?

(Nakoniec sa možno spýtajte aj “Ako často bude táto správa odoslaná?” A preto “Ako najlepšie optimalizovať túto správu?”)

Zatiaľ čo existuje veľa spôsobov, ako to urobiť s Twisted, budem načrtnúť stratégiu, ktorá sa snaží minimalizovať množstvo napísaného kódu (bojovať s náročnosťou tejto úlohy) a zachovať oddelenie požiadaviek na sieť od zvyšku kódu.

Použitím Twisted, musíme urobiť tri veci do triedy, aby bolo možné posielať prípady to cez sieť: zdědiť z twisted.spread.pb.Copyable, zdědiť z twisted.spread.pb.RemoteCopy a zavolať twisted.spread.pb.setUnjellyableForClass () na to [TODO: opýtajte sa niekoho, kto vie Twisted, ak je to naozaj potrebné]. Veci sa môže stať ešte viac skomplikuje, keď vezmeme do úvahy otázky 4 a 5 z nášho zoznamu hore – to sú dáta vyžadujú osobitné formátovanie ho poslať po sieti? Jediné údaje, ktoré nevyžadujú špeciálne formátovanie, sú doslovné typy: reťazec, int, float atď., Žiadne a kontajnery (zoznamy, nUs, dikty).

Počas skúmania udalostí sa vyskytnú dva prípady, či už to nebude vyžadovať preformátovanie a môžeme sa jednoducho zamiešať do pb.Copyable a pb.RemoteCopy, alebo to bude vyžadovať preformátovanie a budeme musieť vytvoriť novú triedu, ktorá má rutinnú zmeniť pôvodné údaje na niečo, čo sa dá odoslať cez sieť. [TODO: odkaz na vysvetlenie Mixins somewhere]

V tomto ďalšom príklade sme kód rozdelili do viacerých súborov. Všetky udalosti sú v udalostiach.py. V sieti.py sa pokúsime odpovedať na všetky vyššie uvedené otázky pre každú udalosť v events.py. Ak môže správa prechádzať od klienta na server, pridáme ho do zoznamu klientToServerEvents a rovnako aj pre zoznam serverToClientEvents. Ak sú dáta v udalosti jednoduché, ako sú celé čísla a reťazce, môžeme sa jednoducho zmiešať – v triedach pb.Copyable a pb.RemoteCopy a zavolať pb.setUnjellyableForClass () na udalosť.

# from network.py 

# -------------------------------------------- ---------------------------------- 
# GameStartRequest 
# Smer: Len klient na server 
MixInCopyClasses (GameStartRequest) 
pb.setUnjellyableForClass (GameStartRequest, GameStartRequest) 
klientToServerEvents.append (GameStartRequest) # -------------------------------------- ---------------------------------------- # CharactorMoveRequest # Smer: Len klient na server # má to dodatočný atribút, smer. je to int, takže je to bezpečné 
MixInCopyClasses (CharactorMoveRequest) 
pb.setUnjellyableForClass (CharactorMoveRequest, CharactorMoveRequest) 
clientToServerEvents.append (CharactorMoveRequest)





Na druhej strane, ak udalosť obsahuje údaje, ktoré nie sú priateľské k sieti, ako napríklad objekt, musíme vykonať náhradnú udalosť, ktorou sa po kábli posiela namiesto originálu. Najjednoduchší spôsob, ako vykonať výmenu, je iba zmena všetkých atribútov udalostí, ktoré boli objekty, na jedinečné celé čísla pomocou funkcie id (). Táto stratégia vyžaduje, aby sme viedli register objektov a ich identifikačné čísla, takže ak dostaneme udalosť zo siete odkazujúcu na objekt podľa jeho ID, môžeme nájsť skutočný objekt.

# from network.py 

# -------------------------------------------- ---------------------------------- 
# GameStartedEvent 
# Smer: 
Trieda servera iba pre klienta CopyableGameStartedEvent (pb.Copyable, pb.RemoteCopy): 
        def __init __ (vlastné, udalosť, register): 
                self.name = "Herná udalosť" 
                self.gameID = id (event.game) 
                register [self.gameID] = event.game 

pb.setUnjellyableForClass (CopyableGameStartedEvent, CopyableGameStartedEvent) 
serverToClientEvents.append (CopyableGameStartedEvent) # ----------------------------------------- ------------------------------------- # CharactorMoveEvent # Smer: Server iba pre klienta




Trieda CopyableCharactorMoveEvent (pb.Copyable, pb.RemoteCopy): 
        def __init __ (vlastné, udalosť, register): 
                self.name = "Charactor Move Event" 
                self.charactorID = id (event.charactor) 
                register [self.charactorID] = udalosť. charactor 

pb.setUnjellyableForClass (CopyableCharactorMoveEvent, CopyableCharactorMoveEvent) 
serverToClientEvents.append (CopyableCharactorMoveEvent)

Je veľmi dôležité, aby boli tieto triedy presne rovnaké ako trieda, ktorú nahrádzajú, ale s predponou “kopírovateľné”. Môžeme vidieť, ako nahradiť pôvodné udalosti s týmito sieťovými verziami v sieti NetworkClientView.Notify v server.py a môžeme vidieť, ako je príjem týchto udalostí spracovaný v PhonyModel.Notify v klient.py.

Vytvorenie komunikačného kanálu

Videli sme, že môžeme posielať falošné správy na server cez interaktívny python shell, ale to, čo naozaj chceme, je grafický klient. Existuje niekoľko krokov k realizácii tohto cieľa. Po prvé, klienti (zákazníci) musia byť upozornení na akékoľvek zmeny stavu servera. Takže budeme potrebovať obojsmernú komunikáciu. Nielen, že klient posiela požiadavky na server, ale aj server oznamuje klientovi udalosti. (To je dôvod, prečo jednosmerné (“pull”) protokoly ako XML-RPC alebo HTTP nie sú vhodné pre naše potreby)

Zo servera je potrebné odoslať zmeny, preto musíme vytvoriť nový pohľad na serveri.

# z server.py 

# -------------------------------------------- ---------------------------------- 
trieda NetworkClientView (objekt): "" "Odosielam udalosti do CLIENTU cez tento objekt "" " 
	def __init __ (vlastné, evManager, sharedObjectRegistry): 
		self.evManager = evManager 
		self.evManager.RegisterListener (self) 
		self.clients = [] 
		self.sharedObjs = sharedObjectRegistry # ---------- -------------------------------------------------- ---------- 
	def Notify ( 
		vlastník , udalosť): if isstance (udalosť, ClientConnectEvent): 
			self.clients.append (event.client) 
		ev = udalosť # neprenáša udalosti, ktoré nie sú kopírovateľné 
		ak nie je inštancia (ev, pb.Kopírovateľné):
	



	


		
			evName = ev .__ trieda __.__ name__ 
			copyableClsName = "Copyable" + evName 
			, ak nie hasattr (siete, copyableClsName): 
				návrat 
			copyableClass = getattr (siete, copyableClsName) 
			ev = copyableClass (ev, self.sharedObjs) 

		v prípade, ev .__ class__ nie je v sieti. serverToClientEvents: 
			#print "SERVER NOT SENDING:" + str (ev) 
			návrat #NOTE: je to veľmi "chat". Mohli by sme obmedziť # počet klientov, ktorí budú v budúcnosti notifikovaní 
		pre klienta v samostatných klientoch: 
			print "===== odosielanie servera:", str (ev) 
			remoteCall = client.callRemote ("ServerEvent", ev)

		
		

Služba NetworkClientView vedie odkaz na register servera, ktorý mapuje objektové ID čísla na skutočné objekty. Má tiež zoznam klientov. Objekty v zozname klientov zdedia z pb.Referentable, takže môžeme použiť metódu callRemote () a posielať správy cez sieť. Zoznam serverToClientEvents sa importuje z siete.py.

NetworkClientView.Notify () sa zaujíma predovšetkým o udalosti, ktoré možno kopírovať. Udalosť prešla do Notify () už môže byť kopírovateľná v dôsledku miešania pb.Copyable v sieti.py. V takom prípade sa isinstance( ev, pb.Copyable )vráti True. Ak to nie je kopírovateľné, v sieťovom module môže existovať náhradná trieda a my môžeme skontrolovať prefixovaním “Copyable” na názov triedy udalosti, pretože sme použili túto konvenciu pomenovania pre triedy výmeny v sieti.py.

Ako je vidieť v NetworkClientView.Notify (), server očakáva, že klient pošle vzdialene prístupný objekt (ako ten, ktorý zdedí z Twistovho pb.Root), keď sa klient pripojí. Potom server môže tento objekt použiť na oznamovanie udalostí klientovi.

Teraz budeme (nakoniec) začať s klientom. Z pohľadu klienta prichádzajúce správy zo servera predstavujú riadiacu jednotku, takže máme triedy NetworkServerController v klient.py. Ako možno očakávate, klient odošle udalosti aj na server cez zobrazenie NetworkServerView.

# z klient.py 

# -------------------------------------------- ---------------------------------- 
trieda NetworkServerView (pb.Root): 
        "" " Odosielame udalosti do priečinka server cez tento objekt "" " 

        ... # --------------------------------------- ------------------------------- 
        def Pripojené (vlastné, serverové): 
                self.server = server 
                self.state = NetworkServerView. STATE_CONNECTED 
                ev = ServerConnectEvent (server) 
                self.evManager.Post (ev) 
        ... # ------------------------------- --------------------------------------- 
        def AttemptConnection (vlastné): 
                ...

        


        
                Pripojenie = self.reactor.connectTCP (serverHost, serverPort, 
                                                     self.pbClientFactory) 
                odložené = self.pbClientFactory.getRootObject () 
                deferred.addCallback (self.Connected) 
                deferred.addErrback (self.ConnectFailed) 
                self.reactor.startRunning () 

        .. . # ------------------------------------------------ ---------------------- 
        def Notify ( 
                vlastník , udalosť): 
                ev = udalosť, ak je inštancia (udalosť, TickEvent): 
                        if self.state == NetworkServerView.STATE_PREPARING :

        

                                self.AttemptConnection () 
                        ...

Na prvom TickEvent, ktorý dostane NetworkServerView, sa pokúsi pripojiť na server. Po vytvorení spojenia sa metóda Connected () volá s odkazom na objekt servera, ktorý zdedí z pb.Referentable, a preto ho klient môže použiť na vzdialený prístup na server. Tiež vytvára ServerConnectEvent.

# z klient.py 

# -------------------------------------------- ---------------------------------- 
trieda NetworkServerController (pb.Referentable): "" " PRIJÍMAME udalosti z server cez tento objekt "" " 
        def __init __ (vlastné, evManager, twistedReactor): 
                self.evManager = evManager 
                self.evManager.RegisterListener (self) # ------------------- -------------------------------------------------- - 
        def remote_ServerEvent (vlastné, udalosť): 
                self.evManager.Post (udalosť) 
                návrat 1 # ------------------------------ ---------------------------------------- 
        def Notify (vlastník, udalosť):
        

        

        
                ak je inštancia (udalosť, ServerConnectEvent): #teli server, ktorý ju počúvame a #it môže pristupovať k tomuto objektu 
                        event.server.callRemote ("ClientConnect", vlastné)
                        
                        

NetworkServerController dostane upozornenie na server ServerConnectEvent a používa ho na prenos obsahu odkazu na server. Teraz môže server volať metódu remote_ServerEvent () NetworkServerController. Takže server i klient majú odkazy na vzdialene vypovedateľné objekty. Toto je kanál, cez ktorý komunikujú.

Príklad aplikácie

Miestna kópia stavu servera

Teoreticky by mal existovať iba jeden model, autoritatívny model na serveri a klienti by mali byť iba zobrazenia a kontroléry pre daný model. Nie je však jednoduché zachovať odkazy na objekt vzdialeného modelu v aplikácii EventManager klienta, pohľady a kontroléry. Mnoho udalostí môže byť spracované úplne aj na strane klienta a vždy ich posielanie na server vytvorí zbytočný hluk. Takže dobrý dizajn by mal vytvoriť lokálny model na zrkadlenie herných objektov, ktoré existujú na strane servera.

Na strane klienta vytvoríme PhonyModel, ktorého stav budeme synchronizovať s autoritatívnym modelom na serveri. Tento PhonyModel poskytuje rovnaké rozhranie ako model servera, ale má osobitnú úlohu – zabezpečiť, aby miestne herné objekty nemenili stav hry, keď nemajú oprávnenie tak urobiť. V našom príklade sa to dosiahlo zachovaním dvoch objektov EventManager, jedného nazývaného phonyEventManager, ktorý jednoducho vyradí udalosti, ktoré dostáva, efektívne umlčia všetky udalosti pochádzajúce z miestnych herných objektov a jeden nazývaný realEventManager, ktorý prepoguje udalosti prijaté zo servera. Udalosti uverejnené v realEventManager sa zobrazia v objektoch Zobraziť, akcie uverejnené v phonyEventManageru nebudú.

Vzhľadom na to, že náš príklad je veľmi jednoduchý, môžeme prekonať túto jednoduchú implementáciu. Je možné si predstaviť situácie, kedy by sme mohli povoliť, aby miestny herný objekt zmenil miestny stav. Toto by sa dalo dosiahnuť tým, že aplikácia PhonyEventManager poskytne tieto špeciálne udalosti. Ďalším prístupom by mohlo byť, aby nemal lokálny model na klientovi, iba objekt Zobraziť, na ktorom prichádzajúce udalosti zo servera mali priamy efekt.

Odosielanie komplexných objektov

Tu je zložitejšia časť: ako môžeme posielať zložité objekty ako Hráči alebo Charactors cez kanál, ktorý sme vytvorili? Toto sa nazýva serializácia . Na serializáciu našich objektov potrebujeme urobiť dve veci.

  • Vytvorte register, ktorý mapuje jedinečné ID na herné objekty
  • Pre každú triedu máte možnosť zmeniť všetky svoje interné údaje na čísla a reťazce a spôsob, ako ich zmeniť späť na užitočné objekty

Získanie unikátnych ID je jednoduché, môžeme použiť výsledok funkcie id () pri volaní na daný objekt na serveri . Musí pochádzať zo servera tak, aby bol jedinečný, inak by sme mali viac identifikátorov pre jeden objekt.

Keď sa udalosti odkazujúce na zložité objekty dostanú do siete NetworkClientView na serveri, objekty sú serializované začínajúce v konštruktore udalosti Copyable.

# z server.py
 
triedy NetworkClientView: 
        ... 

        def Notify ( 
                vlastník , udalosť): 
                ... 

                ev = udalosť, 
ak nie je inštancia (ev, pb.Copyable): 
                        evName = ev.__ trieda __.__ meno__ copyableClsName 
                        = "Copyable" + evName 
                        ak nie hasattr (network, copyableClsName) 
                                vrátiť 
                        copyableClass = getattr (network, copyableClsName ) #Toto seriál začína 
                        ev = copyableClass (ev, self.sharedObjs) elif 
                ev.__ class__ nie v serverToClientEvents:
                        

                        návrat 

                pre klienta v klientoch self.clients: 
                        self.RemoteCall (klient, "ServerEvent", ev)

Zoberme si príklad CharactorMoveEvent. Vyššie uvedený kód zavolá __init __ () pre CopyableCharactorMoveEvent.

# Od network.py
 
triedy CopyableCharactorMoveEvent (pb.Copyable, pb.RemoteCopy): 
        def __init __ (self, udalosti, register): 
                self.name = "Copyable" + event.name 
                self.charactorID = id (event.charactor) 
                register [ self.charactorID] = event.charactor

Ako vidíte, server po odoslaní udalosti nebude odosielať skutočný objekt, bude odosielať iba jedinečné identifikačné číslo. Rovnako sa ubezpečuje, že z tohto identifikátora existuje mapovanie skutočného objektu v databáze Registry.

Keď je klient odoslaný CopyableCharactorMoveEvent, PhonyModel ho vyberie (PhonyModel je jediným objektom, ktorý má záujem o udalosti, ktoré začínajú kódom “Copyable”).

#from class.php client.py
 
PhonyModel 
        ... # --------------------------------------- ------------------------------- 
        def Notify ( 
                vlastník , udalosť): 
                ... if isstance (event, CopyableCharactorMoveEvent): 
                        charactorID = event.charactorID 
                        ak nie 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)

Keď sa kariér pohybuje, je v novom sektore. Ak chcete komunikovať s klientom, server odošle CharactorMoveEvent, ktorý má jeden atribút, samotný charaktor. Klient prijíma túto udalosť, vidí charaktru, na ktorý sa odkazuje, a požaduje nový stav (ktorý sektor je v ňom) pre daného charaktera.

Ide o veľmi všeobecný prístup k riešeniu problému.

  • Server: Hej, kámo X sa práve presťahoval!
  • Klient: Oh, naozaj? Povedz mi všetko, čo teraz o tom chlapíkovi.
  • Server: No, je v stave “ACTIVE” a je v sektore s unikátnym ID 123567.
  • Klient (na seba): Ach, výborný. Už som o tomto sektore vedel, takže jednoducho zmením môj malý model vesmíru a posunu toho frajera do tohto sektora.

Táto konverzácia trvala 4 správy. Mohlo to byť kratšie; mali by sme len ručne vyrobiť CopyableCharactorMoveEvent do niečoho špecifickejšího pre potreby našej hry, napríklad by sme mohli zahrnúť odvetvie ako atribút udalosti, aby sa vyhli žiadosti o ďalšie informácie.

  • Server: Hej, kámo X sa práve presťahoval! Do sektoru 123567!
  • Klient (na seba): Ach, výborný. Už som o tomto sektore vedel, takže jednoducho zmením môj malý model vesmíru a posunu toho frajera do tohto sektora.

Ale kód budeme teraz veľmi generický. Mnohé ďalšie udalosti sa budú riadiť rovnakým vzorom.

Vráťte sa do útržku kódu, ak už klient prijal tento objekt zo servera, self.sharedObjs.has_key()vráti sa pravda a môže získať odkaz na objekt z registra a pokračovať ako normálne. Ak ešte tento objekt nedostal (ako je tomu pri prvom prijatí tejto udalosti), musí najprv vytvoriť objekt zástupného symbolu a potom skopírovať stav objektu na serveri do tohto nového zástupného objektu. Vykoná to tak, že volá GetObjectState () s jedinečným ID potrebného objektu.

GetObjectState () v podstate len zistí, že objekt na serveri (v tomto príklade Charactor, ktorý sa presťahoval), a serializuje jeho dáta s volaním getStateToCopy (). GetObjectState () vráti dict a požadované ID objektu.

# from network.py 

# -------------------------------------------- ---------------------------------- 
trieda CopyableCharactor: 
        def getStateToCopy (vlastné, registru): 
                d = self .__ dict__ .pdf () 
                del d ['evManager'] 
                sID = id (samostatný sektor) 
                d ['sector'] = sID 
                register [sID] = self.sector 
                return d 

        def setCopyableState (self, stateDict, register): 
                requiredObjIDs = ] 
                success = 1 
                if stateDict ['sector'] nie je v registri: 
                        register [stateDict ['sector']] = Sektor (self.evManager)
                        requiredObjIDs.append (stateDict ['sector']) 
                        úspech = 0 
                else: 
                        self.sector = register [stateDict ['sector'] 

                ]

Dikt, ktorý vráti getStateToCopy () obsahuje všetky údaje priateľské k sieti, takže je možné ich odoslať cez sieť.

Klient dostane tieto informácie vo funkcii StateReturned (), čo je pravdepodobne najťažšia funkcia, ktorú možno sledovať v tomto celom tutoriále. Pokúsim sa prejsť krok po kroku.

Klient najprv požaduje stav objektu. Keď príde odpoveď, spätné volania StateReturned a CharactorMoveCallback sú zaradené do frontu, aby sa zavolali postupne.

# from client.py
 
        def Notify ( 
                        vlastník , udalosť): 
                ... 
remoteResponse = self.server.callRemote ("GetObjectState", 
                        charactorID ) remoteResponse.addCallback (self.StateReturned) 
                        remoteResponse.addCallback (self.CharactorMoveCallback)

Prvý spätný hovor, StateReturned, bude nazvaný s [objectID, objDict] ako jeho argument “response”.

# z server.py
 
        def vzdialené_GetObjectState (self, objectID): 
                ... 

                vrátiť [objectID, objDict]
# z klient.py 

        # -------------------------------------------- -------------------------- 
        def StateReturned (self, response): 
                "" "Toto je spätné volanie, ktoré sa volá v reakcii na 
                vyvolanie GetObjectState na server 

                objD, objDict = odpoveď v 
                prípade objID == 0: 
                        vytlačiť "GOT ZERO - lepšia chyba obsluhy tu" 
                        nevrátiť Žiadne 
                obj = self.sharedObjs [objID] 

                úspech, needObjIDs = \ 
                                 obj.setCopyableState (objDict, self. sharedObjs) v 
                prípade úspechu:
                        #we úspešne nastavíme stav a žiadne ďalšie objekty 
                        # sú potrebné na dokončenie aktuálneho objektu, 
                        ak objID v self.neededObjects: 
                                self.neededObjects.remove (objID) 

                else: #to dokončiť aktuálny objekt, musíme chytiť #state z niektoré ďalšie objekty na serveri. Identifikátory # pre tieto potrebné objekty boli odovzdané späť # in needObjIDs 
                        for neededObjID in neededObjIDs: 
                                if neededObjID nie je v self.neededObjects: 
                                        self.neededObjects.append (requiredObjID) 
                self.waitingObjectStack.append ((obj, objDict))
                        
                        
                        
                        
        

                retval = self.GetAllNeededObjects (), 
                ak retval: # retval je odložený - vracia to spôsobí vytvorenie reťazca #. 
                        vrátiť späť
                        
                        

V najjednoduchšom prípade je “úspech” True a GetAllNeededObjects () nevracia okamžite. Potom sa zavolá ďalší hovor, CharactorMoveCallback, a skončili sme.

Ak je však “úspech” nepravdivý, znamená to, že je potrebných viac údajov na dokončenie stavu pôvodne požadovaného objektu. PhonyModel uchováva zoznam potrebných objektov, ktoré musia byť vyžiadané od servera pred dokončením pôvodne požadovaného objektu. Každý z týchto potrebných objektov môže tiež pridať do zoznamu potrebných objektov pre ďalšie objekty, ktoré potrebujú. Takže keď voláme GetAllNeededObjects () začína rekurzívne správanie.

# z klient.py 

        # -------------------------------------------- -------------------------- 
        def GetAllNeededObjects (self): 
                ak len (self.neededObjects) == 0: # toto je rekurzia-koniec podmienka. Ak je z servera potrebných #no viac objektov, potom sa ich môžeme pokúsiť znova nastaviťCopyableState a #weby by mali teraz mať všetky potrebné objekty a zabezpečiť, že # setCopyableState sa úspešne 
                        vráti self.ConsumeWaitingObjectStack () #still v rekurzívny krok. Snažte sa získať objektový objekt pre #identiID v hornej časti zásobníka. Všimnite si, že rekurzia # je vykonaná cez odložený, čo môže byť mätúce
                        
                        
                        
                        
                        

                
                
                
                nextID = self.neededObjects [-1] 
                remoteResponse = self.server.callRemote ("GetObjectState", nextID) 
                remoteResponse.addCallback (self.StateReturned) 
                vrátiť remoteResponse

Ako môžete vidieť, uskutoční sa na serveri GetObjectState ďalší hovor, ktorý bude mať za následok vyvolanie StateReturned. Všimnite si, že to nie je skutočne rekurzívny. GetAllNeededObjects neblokuje. Okamžite sa vráti. Vracia však objekt Odložený, remoteResponse. Takže originál Deferred bol nazvaný ako prvé spätné volanie a vrátil sa nový odložený objekt. Toto sa nazýva Chaining Deferreds a spôsobí, že prvé spätné volanie sa zablokuje, kým nebudú ukončené druhé spätné volania služby Deferred. Z tohto dôvodu dochádza k rekurziu v sieti.

Tu je vývojový diagram, ktorý sumarizuje kroky vykonané, keď klient dostane udalosť obsahujúcu komplexný objekt.

vývojový diagram prijímania udalostí klientaVšimnite si, že sa musíme uistiť, že udalosť, ktorú pošleme po sieti, má dostatok informácií na aktualizáciu klienta s akýmikoľvek relevantnými zmenami stavu servera. Klient môže mať už lokálnu verziu objektu, ale ak sa tento objekt zmenil , klient musí stále volat GetObjectState (), ako to dokazuje program CharactorMoveEvent.

Vzhľadom na to sa kladie otázka: kde položíme inteligenciu, určujú, aké objektové stavy potrebujeme na získanie? Práve teraz sme všetky tieto logiky v PhonyModel.Notify () [TODO: je toto najlepšie miesto? a čo vo vnútri kopírovateľné udalosti?]

Ďalšie problémy

Predchádzajúca diskusia je dobrý začiatok a poskytuje nejaký užitočný kód. Povzbudzujem vám, aby ste si s ním zahrali a uvidíte, či môžete dostať svoju hru posielaním predmetov tam a späť. Keďže sa váš kód stáva zložitejším, narazíte na ďalšie problémy:

  1. Čo keď nemáme dostatok informácií na zavolanie __init__ pre niektoré atribúty v setCopyableState ()?
  2. Čo ak nevieme špecifickú podtriedu atribútu v setCopyableState ()?

Ak chcete objasniť, tu je príklad toho, kedy môže prísť taká záležitosť. Povedzme, že píšeme hru, kde sa dvaja tučniaci bojujú navzájom. Každý Penguin má zbraň a každá zbraň je inicializovaná s menom ako “Deathbringer” alebo “Destroy-o-Matic”, alebo “Daffodil”.

# ------------------------------------------------- ----------------------------- 
trieda Zbraň: 
    def __init __ (vlastné, evManager, meno) 
        self.evManager = evManager 
        self.name = názov

CopyablePenguin by takto vyzeral takto:

# ------------------------------------------------- ----------------------------- 
trieda CopyablePenguin: 
    def getStateToCopy (self, register): 
        d = vlastné .__ dict __. copy () 
        del d ['evManager'] 

        wID = 
        register (self.weapon) [wID] = self.weapon 
        d ['weapon'] = wID 
                                                                                
        return d

Naivne by sme začali písať príslušnú funkciu setCopyableState:

    def setCopyableState (self, stateDict, registre): 
        neededObjIDs = [] 
        success = 1 

        WID = stateDict [ 'zbraň'] 
        Ak nie je uvedené registry.has_key (WID): #registry nemal objekt, takže vytvorenie novej jeden 
            ja. weapon = Weapon (self.evManager, #WELL CRAP! Ešte neviem, aké je jeho meno, tak ako to budem inicializovať?
            
            
            

Navyše, povedzme, že zbrane sú jednou z troch podtried, či Slingshot, Rifle alebo Nuke. Potom máme ešte väčšie problémy s setCopyableState:

        ... 
        wID = stateDict ['weapon'], 
        ak nie register.has_key (wID): #registry nemal objekt, tak vytvoriť nový 
            one.weapon = ??? #MORE CRAP! Ani neviem, akú triedu objektu by mala byť!
            
            
            

Tento problém môžeme vyriešiť pomocou zástupného objektu, ktorý je veľmi podobný dizajnu modelu Lazy Proxy .

… [TODO: dokončiť túto časť]

multiplayer

Net-Networked Multiplayer

Začneme tým, že vytvoríme hru pre dvoch hráčov, ktorá bude fungovať lokálne, nie cez sieť. Naša voľne prepojená architektúra nám to umožňuje a je veľkou výhodou, aby sme mohli najprv rozvíjať svoje myšlienky a neskôr sa obávať problémov so sieťou.

Príklad aplikácie Príklad aplikácie

Pridáme pár nových udalostí, PlayerJoinRequest, PlayerJoinEvent (objekt Hráča už nie je vytvorený, keď je hra skonštruovaná) a CharactorPlaceRequest. KlávesnicaController je takisto upravená tak, aby detekovala nové stlačenia tlačidiel , p a c, aby vypálila udalosti vyžiadania a tlačidlo o na prepnutie medzi aktívnymi prehrávačmi. (pozri snímku obrazovky vyššie).

Môžete to vyskúšať spustením python example.pyz nižšie uvedeného archívu example4.tar.gz. Keď sa spustí, dvakrát stlačte tlačidlo p, aby ste požiadali o 2 udalosti Joe, potom stlačte medzerník na spustenie hry, potom stlačte c pre vloženie jedného znaku, o pre prepnutie na iný prehrávač, potom c opäť umiestnite druhý znak. Smerové klávesy posúvajú charakter, ako obvykle.

Networked Multiplayer

Chceme zabezpečiť, aby hráč Player One nedokázal ovládať charakteristiku hráča druhého. Chceme, aby server odmietol akúkoľvek požiadavku, kde inštancia hráča obsiahnutá v žiadosti nie je inštanciou, ktorú môže odosielateľ ovládať. Ako prvý krok musíme byť schopní jednoznačne identifikovať klientov. Potom je potrebné mapovať klientov na súbor objektov prehrávača (alebo viac obyčajne, len jedno), ktoré môžu kontrolovať. Potom je potrebné vyfiltrovať všetky udalosti, ktoré by nemali byť povolené na základe tejto mapy.

Našťastie Twisted poskytuje bohatú sadu nástrojov na identifikáciu klientov, aka “overovanie”. Väčšina z toho je vysvetlená v autentifikácii [TODO] s Perspective Brokerom v Twisted docs. V našom príklade prejdem konkrétne použitie, ale tiež by ste mali tieto dokumenty skontrolovať.

Našou prvou zmenou bude zmena servera NetworkClientController z objektu pb.Root na objekt pb.Avatar:

# z
 
adresy server.py NetworkClientController (pb.Avatar): "" " PRIJÍMAME udalosti od 
        klienta prostredníctvom tohto objektu 
        Existuje inštancia NetworkClientController pre každého pripojeného klienta. 
        " " 
        def __init __ (vlastné, evManager, avatarID, realm) : 
                self.evManager = evManager 
                self.evManager.RegisterListener (vlastné) 
                self.avatarID = avatarID 
                self.realm = realm 
        ... # ---------------------- ------------------------------------------------ 
        def outlook_GetGameSync (vlastné): 
                ...
        


        

        # ------------------------------------------------- --------------------- 
        def perspektíva_GetObjectState (self, objectID): 
                ... # ----------------- -------------------------------------------------- --- 
        def perspective_EventOverNetwork (vlastník, udalosť): 
                ...

        

Ako vidíte, trieda teraz zdedí z pb.Avataru a metódy, ktoré boli predtým nazývané remote_BlahBlah, sú teraz označované ako perspektíva_BlahBlah. Objekty siete NetworkClientController budú musieť sledovať aj ich oblasť a ich ID. Región je v podstate továreň na serveri, ktorá získava požiadavky na nové klientske pripojenia a vytvára nové NetworkServerViews a NetworkServerControllers pre každé úspešné pripojenie.

# zo server.py
 
triedy MyRealm: 
        implements ( 
        portal.IRealm ) def __init __ (vlastné, evManager): 
                self.evManager = evManager # sledovať avatary, ktoré boli vydané 
                self.claimedAvatarIDs = [ # # musíme držať na zobrazeniach takže nezískali odpadky 
                self.clientViews = [] # mapy avatary hráčom (ov), ktoré ovládajú 
                vlastnými hráčmiControlledByAvatar = {} # ------------------- -------------------------------------------------- - 
        def žiadosťAvatar (self, avatarID, mind, * interfaces): 
                ak pb.IPperspective nie je v rozhraniach:
                
                
                

        
                        zvýšiť NotImplementedError, 
                ak avatarID v self.claimedAvatarIDs: # niekto už má tento avatar. 
                        zdvihnite výnimka ( 'Ďalšie klient je už pripojený' 
                                         'k tomuto avatar') 
                self.claimedAvatarIDs.append (avatarID) 
                EV = ClientConnectEvent (myseľ, avatarID) 
                self.evManager.Post (ev) 
                self.playersControlledByAvatar [avatarID] = [] 
                Pohľad = NetworkClientView (self.evManager, avatarID, mind) 
                ovládač = NetworkClientController (self.evManager, 
                                                     avatarID,
                        


                                                     vlastné) 
                self.clientViews.append (zobrazenie) 
                return pb.IPperspective, controller, controller.clientDisconnect # ----------------------------- ----------------------------------------- 
        def knownPlayers (self): 
                ... # ------------------------------------------------- --------------------- 
        def Notify ( 
                vlastník , udalosť): if isstance (udalosť, ClientDisconnectEvent): 
                        self.claimedAvatarIDs.remove (event.avatarID) 
                        removee = Žiadne 
                        pre zobrazenie v self.clientViews:

        

        
                                ak view.avatarID == event.avatarID: 
                                        removee = zobraziť 
                        if removee: 
                                self.clientViews.remove (removee)

Ak sa pozriete na telo metódy requestAvatar, uvidíte, kde sa vytvoria zobrazenia siete a kontrolóri. Metóda requestAvatar je takisto, kde sa do hry vracia avatarID. Vytvára sa interne do spoločnosti Twisted a prechádza do nášho kódu. Jedná sa o identifikátor zaručený ako jedinečný pre každého klienta. Účinne je to “používateľské meno”.

requestAvatar dostane volanie v dôsledku volania prihlásenia () počas metódy AttemptConnection klienta:

# Od client.py
 
avatarID = Žiadne 

def hlavnej (): 
    globálna avatarID 
    ak ľan (sys.argv)> 1: 
        avatarID = sys.argv [1] 
    iné: 
        avatarID = 'user1' 


trieda NetworkServerView (pb.Root): "" "Odošleme udalosti na server prostredníctvom tohto objektu" "" 
    ... # --------------------------------- ------------------------------------- 
    def __init __ (vlastné, evManager, sharedObjectRegistry): 
            self.evManager = evManager 
            self.evManager.RegisterListener (vlastné) 
            self.pbClientFactory = pb.PBClientFactory () 
            self.state = NetworkServerView.STATE_PREPARING 
            self.reaktor = žiadny
    
    

            self.server = Žiadne 

            self.sharedObjs = sharedObjectRegistry # --------------------------------------- ------------------------------- 
    def AttemptConnection (vlastné): 
            self.state = NetworkServerView.STATE_CONNECTING 
            ak self.rector: 
                    self .rector.stop () 
                    self.PumpReactor () 
            else: 
                    self.reactor = SelectReactor () pripojenie 
                    installReactor (self.reactor) 
            = self.reactor.connectTCP (serverHost, serverPort, 
                                                 self.pbClientFactory)

    
            userCred = poverenia.Uznačka používateľa_password (avatarID, 'pass1') 
            kontroler = NetworkServerController (self.evManager) 
            deferred = self.pbClientFactory.login (userCred, client = controller) 
            deferred.addCallback 
            (self.Connected) 
            self.reactor.startRunning () # ------------------------------------------ ---------------------------- 
    def Odpojenie (vlastné): 
            ak nie self.rector: 
                    návrat 
            self.reactor.stop () 
            self. PumpReactor () 
            self.state = NetworkServerView.STATE_DISCONNECTING

    

    # ------------------------------------------------- --------------------- 
    def Pripojené (vlastné, serverové): 
            self.server = server 
            self.state = NetworkServerView.STATE_CONNECTED 
            ev = ServerConnectEvent (server) 
            self.evManager .Post (ev) # -------------------------------------------- -------------------------- 
    def ConnectFailed (vlastné, serverové): 
            self.state = NetworkServerView.STATE_DISCONNECTED

    

Teraz, keď sme tieto používateľské mená diktovali spoločnosťou Twisted, mohli by sme tiež použiť informácie v našom modeli. [TODO: rozbaliť …]

Všetko, čo zostáva, je zmena funkcie KeyboardController. Ovládač KeyboardController sleduje, ktorý hráč je “aktívny” a ovláda iba daného prehrávača a prepína sa po stlačení tlačidla “o”. To funguje dobre, keď beží ako jediný proces, ale teraz, keď existuje niekoľko klientov a ktorý hráč, ktorý ovláda klient je regulovaný serverom, musíme nastaviť KeyboardController.

Najprv budeme dodávať konštruktorovi voliteľný argument “playerName”. Vytvorením predvolenej hodnoty Žiadne, môžeme skontrolovať, či je nastavená a kdekoľvek nie je nastavená, udržiavame jedno-procesné správanie. [TODO: vložiť do kódu] Jedinou zmenou, ktorú urobíte, je reakcia na hráč JoinEvent. V režime s jedným procesom má zmysel vždy ovládať nového prehrávača, ale s viacerými klientmi by mohol tento nový prehrávač pochádzať zo vzdialeného hostiteľa a server nenechá tento lokálny hostiteľ ovládať. Takže sa pokúste ovládať iba hráčov, ktorí sa zhodujú s názvom prehrávača. Nasledujúca otázka môže byť “kde sa nastaví názov_práva”. Je to jednoducho počas hlavnej funkcie () kódu klienta. [TODO: vložiť do kódu]

[TODO: Potrebujem sekciu o tom, prečo reťazec odložiť, keď klient prijíma udalosti zo servera. Pri prijatej udalosti začne klient získavať zo servera nové informácie o stave. Z dôvodu asynchrónneho charakteru sieťových programov a výberu, ktorý sme urobili, aby sme neodoslali VŠETKY potrebné informácie naraz, existujú body v čase, keď sme zo servera zhromaždili neúplné informácie. Ak sme našu falošnú modelu vložili s týmito neúplnými informáciami a potom sa používateľské rozhranie dostalo začiarknuté, pravdepodobne to spôsobí zlyhanie alebo aspoň chybu používateľského rozhrania. Takže budeme odkladať reťazce, získať všetky štátne informácie, ktoré potrebujeme, a akonáhle to bude všetko zhromaždené, potom aktualizujeme náš falošný model a uverejníme udalosti. ]

[TODO: Potrebujem tu sekciu, ktorá hovorí o tom, ako spresniť kód klienta, takže nepotrebujete čakací front. V podstate s lepšou triede Placeholder a niektorými Python self .__ class__ = foo magic, nemusím držať frontu a upevňovať zástupné symboly po stiahnutí všetkého. ]

Opätovné pripojenie po páde

Ako je zvykom na internete, niekedy sa náhodne spustí spojenie. Je vždy príjemné nechať hráčov znovu pripojiť. Kľúčom k tomu je správa GameSync.

GameSync je požiadavka od klienta, aby získal dostatočné informácie o hre, aby znovu vytvoril svoj aktuálny stav z ničoho. V našom príklade len posielame objekt hry z autoritatívneho modelu. Začnite vytvorením novej udalosti, GameSyncEvent:

# z udalosti.py
 
triedy GameSyncEvent (udalosť): 
    def __init __ (self, game): 
        self.name = "Game Synched to autoritative štát" 
        self.game = hra

Pridajte na server ďalšiu metódu vzdialeného vypovedania a kód na strane servera je hotový:

# z
 
platformy server.py NetworkClientController (pb.Avatar): 

    ... 

    def perspekt_GetGameSync (self): "" Toto sa zvyčajne nazýva, keď sa klient najprv pripája alebo 
        keď sa znovu pripojí po 
        pokuse 
        "" " game = sharedObjectRegistry.getGame ) 
        if game == Žiadne: 
            zvýšiť Výnimku ('Hra by mala byť nastavená týmto bodom') 
        gameID = id (game) 
        gameDict = game.getStateToCopy (sharedObjectRegistry) 
        návrat [gameID, gameDict]
        

Ďalej musíme klienta pripojiť. Kedy má byť GameSync požiadaný? Musí to byť vykonané v čase, keď klient má pripojenie k serveru, ale model na strane klienta (PhonyModel) ešte nebol naplnený. Dobrým miestom je obslužný program ServerConnectEvent v samotnom PhonyModel.

# from client.py
 
class PhonyModel: 

    ... 

    def Notify ( vlastník , udalosť): 
        if isstance (udalosť, ServerConnectEvent): 
            self.server = event.server # pri opätovnom pripojení na server by sme mali dostať #entire stav hry , 
            ak nie self.game: 
                self.game = Hra (self.phonyEvManager) 
                gameID = id (self.game) 
                self.sharedObjs [gameID] = self.game 
            remoteResponse = self.server.callRemote ("GetGameSync") 
            remoteResponse.addCallback self.GameSyncReturned) 
            remoteResponse.addCallback (self.GameSyncCallback, ID hry)
            
            
            remoteResponse.addErrback (self.ServerErrorHandler, 'ServerConnect') 

        ...

Upozorňujeme, že dva príkazy na spätné volanie sú pripojené k vzdialenejResponse programu GetGameSync. Ako sme už videli, znamená to, že funkcie GameSyncReturned a GameSyncCallback budú vykonané postupne.

Tieto dve funkcie sú jednoduché; jednoducho vyplnia zdieľanéObjs na strane klienta a posielajú GameSyncEvent správcovi udalostí na strane klienta.

# from client.py
 
class PhonyModel: 

    ... de 

    GameSyncReturned (self, response): 
        gameID, gameDict = odpoveď 
        print "GameSyncReturned:", gameID 
        self.sharedObjs [gameID] = self.game # StateReturned vráti odložený, aby sa udržal reťaz #. 
        návrat self.StateReturned (odpoveď) 
    ... 
    def GameSyncCallback (vlastné, odloženéResult, gameID): 
        game = self.sharedObjs [gameID] 
        ev = GameSyncEvent (hra) 
        self.realEvManager.Post (ev)
        
        


Posledný detail o opätovnom pripojení zahŕňa example.py. Po opätovnom pripojení klienta bude mať nový objekt KeyboardController. Tento KeyboardController neobdrží PlayerJoinEvent na nastavenie aktívneho prehrávača, pretože hra už prebieha (v autoritatívnom modeli sa obaja hráči už pripojili). Takže pridáme nový príklad do example.py (ohrozujeme princípy stanovené v programe Rapid Development stranou, ale bude to neškodné, sľubujem.) (Môže niekto navrhnúť lepší spôsob, ako to urobiť?) ), Aby si vzal GameSyncEvent a zistite, ktorý hráč má ovládač KeyboardController ovládať.

# Od example.py
 
triedy KeyboardController: 

    ... 

    def Informovať (self, event): 

        ... 

        ak isinstance (event, GameSyncEvent): 
            hra = event.game 
            self.players = game.players [:] # kópie zoznamu 
            if self.playerName a self.players: 
                self.activePlayer = [p pre p v self.players, 
                                     ak p.name == self.playerName] [0] 
        ...

V tomto okamihu môžete otestovať opätovné pripojenie. Extrahujte kód z príkladu4.tar.gz a otvorte 3 terminály. V jednom termináli spustite server.py. V termináli 2 spustite `python client.py user1`. V termináli 3 spustite `python client.py user2`. Vytvorte prehrávača v každom okne Pygame. Potom spustite hru stlačením medzerníka v okne Pygame. Vytvorte charactor v každom okne Pygame a presuňte charaktery na nové pozície. Potom zatvorte druhé okno Pygame. Mali by ste vidieť, že server reaguje tlačou niektorých správ o odpojení. Teraz spustite znova `python client.py user2`. Klient by sa mal pripojiť, získať stav hry a zobraziť charaktery v rovnakých polohách, ktoré sú zobrazené v prvom okne Pygame. Mali by ste byť schopní ovládať charactor 2 znova.

ČASŤ 3

Grafické užívateľské rozhranie

Čo je to widget

Widget je elementárny objekt v grafickom rozhraní. Widget môže byť tlačidlo, štítok, pole pre zadávanie textu atď. Widget môže obsahovať aj iné miniaplikácie, ako napríklad panel s nástrojmi alebo menu, alebo dokonca jednoduchú horizontálnu schránku.

Pri vytváraní vášho grafického rozhrania môžete získať toľko komplikovanejšie, ale tento návod sa zameria len na niektoré jednoduché miniaplikácie. Tu sú tie, ktoré implementujeme:

  • štítok
  • gombík
  • pole pre zadávanie textu
  • [TODO … scrollbox?]

Všetky miniaplikácie zdieľajú malé množstvo správania, takže máme abstraktnú triedu Widget, ktorú zdedí Sprite. Widgety môžu byť zamerané a rozostavené a majú príznak “špinavý”, aby mohli byť v prípade potreby prekreslené, a nie pri každom hovore na aktualizáciu ().

# ------------------------------------------------- ----------------------------- 
trieda Widget (pygame.sprite.Sprite): 
    def __init __ (vlastné, evManager, kontajner = Žiadne) : 
        pygame.sprite.Sprite .__ init __ (vlastné) 

        self.evManager = evManager 
        self.evManager.RegisterListener (self) 

        self.container = kontajner 
        self.focused = 0 
        self.dirty = 1 # ---------- -------------------------------------------------- ---------- 
    def SetFocus (vlastné, val): 
        self.focused = val 
        self.dirty = 1 # --------------------- ------------------------------------------------- 
    def zabiť (vlastné): 
        self.container = Žiadne

    

    
        del self.container 
        pygame.sprite.Sprite.kill (vlastné) # ----------------------------------- ----------------------------------- 
    def Notify (vlastné, udalosť): 
        if isstance (udalosť, GUIFocusThisWidgetEvent) \ 
           a event.widget je vlastný: 
            self.SetFocus (1) 
        elif isinstance (event, GUIFocusThisWidgetEvent) \ 
             a self.focused: 
            self.SetFocus (0)

    

štítok

Pravdepodobne najjednoduchší widget je štítok. V podstate je len držiteľom nejakého textu.

# ------------------------------------------------- ----------------------------- 
trieda LabelSprite (Widget): 
    def __init __ (vlastné, evManager, text, kontajner = Žiadne): 
        Widget .__ init __ (vlastné, evManager, kontajner) 

        self.color = (200,200,200) 
        self.font = pygame.font.Font (Žiadne, 30) 
        self .__ text = text 
        self.image = self.font.render (self. , self.color) 
        self.rect = self.image.get_rect () # --------------------------------- ------------------------------------- 
    def SetText (vlastné, textové): 
        self .__ text = text 
        self.dirty = 1 # -------------------------------------------- --------------------------

    

    
    def aktualizácia (vlastné): 
        ak nie samoštítko: 
            návrat 

        self.image = self.font.render (vlastný .__ text, 1, self.color) 
        self.dirty = 0

gombík

Tlačidlo je tiež veľmi jednoduchý widget. Je to len obrázok, na ktorý je možné kliknúť, a keď sa naňho klikne, vypne udalosť. Pre jednoduchosť je obrázok len nejaký vykreslený text, ale môže to byť čokoľvek.

# ------------------------------------------------- ----------------------------- 
trieda ButtonSprite (Widget): 
    def __init __ (vlastné, evManager, text, kontajner = Žiadne, onClickEvent = Žiadne): 
        Widget .__ init __ (vlastné, evManager, kontajner) 

        self.font = pygame.font.Font (Žiadne, 30) 
        self.text = text 
        self.image = self.font.render (self.text, , 0,0)) 
        self.rect = self.image.get_rect () 

        self.onClickEvent = onClickEvent # --------------------------- ------------------------------------------- 
    def aktualizácia (vlastné): 
        ak nie self.dirty: 
            return 
        if self.focused: 
            color = (255,255,0)

    

        inak: 
            color = ( 
        255,0,0 ) self.image = self.font.render (self.text, 1, farba) # self.rect = self.image.get_rect () 
        self.dirty = 0 # --- -------------------------------------------------- ----------------- 
    def Connect (self, eventDict): 
        pre kľúč, udalosť v prípadeDict.iteritems (): 
            try: 
                self .__ setattr __ (kľúč, udalosť) 
            okrem AttributeError: 
                print "Nepodarilo sa pripojiť tlačidlo", kľúčový 
                priečinok # --------------------------------------- ------------------------------- 
    def Kliknite (vlastné): 
        self.dirty = 1, 
        ak self.onClickEvent:
        


    


    
            self.evManager.Post (self.onClickEvent) # --------------------------------------- ------------------------------- 
    def Notify (vlastné, udalosť): 
        ak je inštancia (udalosť, GUIPressEvent) a self.focused : 
            self.Click () 
        Elif isinstance (event, GUIClickEvent) \ 
             a self.rect.collidepoint (event.pos): 
            self.Click () 
        Elif isinstance (event, GUIMouseMoveEvent) \ 
             a self.rect.collidepoint (event.pos) : 
            ev = GUIFocusThisWidgetEvent (vlastné) 
            self.evManager.Post (ev) 
        Widget.Notify (vlastník, udalosť)

    




Textové pole

Textové pole je o niečo zložitejšie, ale stále ľahko pochopiteľné. Ide v podstate o obdĺžnik, do ktorého môže byť text napísaný. Keď sa zaostrí, zobrazí malý zvislý pruh (|) a začne reagovať na udalosti stlačenia klávesov.

# ------------------------------------------------- ----------------------------- 
trieda TextBoxSprite (Widget): 
    def __init __ (vlastné, evManager, šírka, kontajner = Žiadne): 
        Widget .__ init __ (vlastné, evManager, kontajner) 

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

        self.rect = pygame.Rect (0,0, šírka, )) 
        boxImg = pygame.Správna (self.rect.size) .convert_alpha () 
        color = (0,0,100) 
        pygame.draw.rect (boxImg, farba, self.rect, 4) 

        self.emptyImg = boxImg.convert_alpha 
        vlastné.image = boxImg 

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

    # ------------------------------------------------- --------------------- 
    def update (self): 
        ak nie self.dirty: 
            return 

        text = self.text 
        ak 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 Kliknite (vlastné): 
        self.focused = 1 
        self.dirty = 1 # ------ -------------------------------------------------- --------------

    

    
    def SetText (vlastné, novéText): 
        self.text = newText 
        self.dirty = 1 # ------------------------------- --------------------------------------- 
    def Notify ( 
        vlastník , udalosť): if isstance ( event, GUIPressEvent) a self.focused: 
            self.Click () 
        elif isinstance (event, GUIClickEvent) \ 
             a self.rect.collidepoint (event.pos): 
            self.Click () 
        Elif isinstance (event, GUIClickEvent) \ 
             a ja. zamerané: 
            self.SetFocus (0) 
        elif je inštancia (udalosť, GUIMouseMoveEvent) \ 
             a self.rect.collidepoint (event.pos):

    




            ev = GUIFocusThisWidgetEvent (vlastný) 
            self.evManager.Post (ev) 

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

        elif isinstance (event, GUIControlKeyEvent) 
          a self.focused a event.key == K_BACKSPACE: #strip posledného znaku 
            newText = self.text [:( len (self.text) - 1)] 
            self.SetText (newText) 
        Widget.Notify (self, event)
            

GUI obrazovky

vizuálne stavy príkladovej hryAbstrakt je schéma zobrazujúca niektoré bežné použitia grafického používateľského rozhrania v hrách. V každej z vyššie uvedených obrazoviek sa nachádza modrá časť, ktorá predstavuje tlačidlá alebo iné miniaplikácie. Bude tiež slúžiť ako myšlienka pre ďalšiu príkladovú aplikáciu “Fool The Bar”.

Ponuka

príklad ponukyGUI Menu je prvou vecou, ktorá sa zobrazí pri spustení programu. Obvykle je to len zbierka tlačidiel, najčastejšie veci ako “Nová hra”, “Ukončiť” a “Možnosti”. Niekedy existuje niekoľko možností “Možnosti”.

možnosti

príkladVoľba GUI je miesto, kde používateľ nastavuje svoje predvoľby alebo pridáva svoje osobné údaje. Obvykle sú to textové štítky s priľahlými políčkami, kde používateľ môže zmeniť / pridať hodnoty.

Hlavná

hlavným príkladomHlavné GUI je miesto, kde sa hra skutočne hrá. Niekoľko hier má veľa spoločného, pokiaľ ide o hlavné grafické rozhranie. Mnohé hry však majú “funkčný panel” alebo “klávesovú skratku” pozdĺž niektorých okrajov, ktoré tvoria tlačidlá alebo iné miniaplikácie.

cutscén

príkladCutscene je súčasťou hry, kde je odobratá priama kontrola a je tu časť príbehu hry. Môžete to urobiť prehrávaním filmu alebo prezentovaním textu so sprievodnými obrázkami. Vstup pre používateľov je zvyčajne obmedzený na niekoľko možností, ako napríklad “preskočiť” alebo “pokračovať”.

dialóg

príklad dialóguDialog je jedna z najnáročnejších vecí, ktoré je potrebné urobiť v hre. Zvyčajne je to obdĺžnik, ktorý sa objaví nad hlavným grafickým rozhraním obsahujúcim tlačidlá, text alebo iné miniaplikácie (ako dialóg RPG “inventár”). Zatiaľ čo dialóg je hore, prezentácia hlavného grafického používateľského rozhrania sa zvyčajne neprerušuje (hoci by to mohlo byť). Veľa sa môže v pozadí pohybovať, ale dialóg sa chápe ako zameraný . Ak je napríklad prítomné dialógové okno “chat”, stlačenie tlačidiel, ktoré zvyčajne spôsobujú presun Charactor (napr. “WASD”), prejde iba do dialógového okna “chat”, aby mohol používateľ napísať správu. Ak sa dialógové okno “áno / nie” objaví tak, že tlačidlo “nie” je nad znakom na obrazovke a používateľ klikne na tlačidlo “nie”, toto kliknutie by nemalo vyberať znak, ktorý je pod ním, stlačte tlačidlo “nie”.

FAQ

  • Ktorá licencia je celý tento kód pod?
  • Pokiaľ nie je uvedené inak, všetko tu je vo verejnej oblasti.
  • Prečo používate from module import *? Nevieš, že je to zlý štýl kódovania
  • Áno, moja zlá. Sľubujem, že budem vyčistiť všetky tie predtým, než vyhlásím toto “hotové”.
  • Prečo nepoužívate Twisted’s Cacheable? Vyzerá to, že ste to práve znova dopĺňali.
  • Som trochu nováčik, pokiaľ ide o Twisted. Čítal som trochu o možnosti uloženia do vyrovnávacej pamäte, ale zdalo sa mi, že by som musel urobiť triedy môjho modelu Cacheable a ja som nechcel, aby bol sieťový kód spojený s mojím modelovým kódom. A keďže by som musel napísať serializačný kód, aj keby som použil Cacheable, len som bol bulldozed dopredu. Bolo by možné použiť funkciu Cacheable bez toho, aby ste ju pripojili k modelovému kódu? Mohol by mi niekto, kto je s Twisted známy, ma správnym smerom?

Leave a Reply

Your email address will not be published. Required fields are marked *