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

Учебник для начинающих sjbrown's

Шенди Браун. Пожалуйста, отправьте комментарии / исправления по электронной почте на адрес tutorial@ezide.com.

Последнее обновление: март 2011 г.


Содержание

То, что вы должны знать

Это руководство предполагает определенный уровень знаний. Если вы считаете, что это сбивает с толку, либо вы должны разобраться в некоторых из этих концепций, либо я должен стать лучшим автором. Это как-то ставит нас в гонку вооружений лень.

Объектно-ориентированного программирования

Ожидается, что читатель будет удобен в объектно-ориентированной среде. Весь важный код структурирован с помощью классов.

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

Шаблоны проектирования - инструмент коммуникации; Они не диктуют дизайн, они информируют о чтении кода. В этом руководстве используются шаблоны проектирования «Контроллер представления модели» (MVC), «Посредник» и «Леновый прокси». Время не будет потрачено, описывая эти шаблоны подробно, поэтому, если они кажутся вам чуждыми, я рекомендую проверить книгу «Шаблоны проектирования» от Gamma et al. Или просто серфинг в Интернете для учебных пособий.

ЧАСТЬ 1

Пример цели

Это всегда хорошая идея, чтобы набросать свою игру либо с картинками или текстом, прежде чем начинать кодирование.

Начнем с того, что мы попытаемся создать программу, в которой маленький человек перемещается по сетке из девяти квадратов. Это слишком простой пример, но легко расширяемый, поэтому мы не будем привязаны к правилам игры, вместо этого мы можем сосредоточиться на структуре кода.

Пример применения

Архитектура

Контроллер просмотра модели

Выбор MVC должен быть довольно очевидным, если речь идет о графической игре. Основная модель будет рассмотрена ниже под заголовком «Модель игры» . Первым представлением будет окно PyGame, отображающее графику на мониторе. Первичным контроллером будет клавиатура, поддерживаемая внутренним модулем Pygame.event PyGame.

Мы еще не добрались до Модели, и уже у нас есть трудности. Если вы знакомы с использованием PyGame, вы, вероятно, привыкли видеть основной цикл следующим образом:

 #stolen from the ChimpLineByLine example at pygame.org
 main():
    ...
    while 1:

        #Handle Input Events
        for event in pygame.event.get():
            if event.type == QUIT:
                return
            elif event.type == MOUSEBUTTONDOWN:
                fist.punch()
            elif event.type == MOUSEBUTTONUP:
                fist.unpunch()

        #Draw Everything
        allsprites.update()
        screen.blit(background, (0, 0))
        allsprites.draw(screen)
        pygame.display.flip()
В этом примере контроллер (часть «События ввода вручную») и View (часть «Рисовать все») тесно связаны, и, как правило, как работают игры PyGame, на каждой итерации основного цикла ожидается Что мы будем проверять входные события, обновлять все видимые спрайты и перерисовывать экран. Опыт говорит нам, что по мере роста кода этот раздел станет волосатым. Организуя это в шаблоне MVC, мы разделяем View и Controller. Наше решение состоит в том, чтобы ввести функцию Tick (), которую основной цикл непрерывного цикла может вызывать как View, так и Controller. Таким образом, не будет кода, специфичного для просмотра, в том же месте, что и код, специфичный для контроллера. Вот пример:
 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():
    #Draw Everything
    ...

 main():
    ...
    while 1:

        if not ControllerTick():
            return

        ViewTick()
Вот еще информация о шаблоне MVC : MVC @ Wikipedia MVC @ ootips.org

обоснование

Читатели, у которых есть опыт написания игр, могут перестать думать, думая, что MVC более сложный, чем необходимый, и что он добавит ненужные накладные расходы, особенно когда цель состоит в том, чтобы создать простую аркадную игру. Теперь исторически, аркадные игры были именно такими, игры, написанные для аркадных машин. Код работал «рядом с металлом» и сжимал все ресурсы машины, чтобы получить трехцветный призрак, который будет мигать синим каждый другой кадр. В 21 веке у нас есть богатые ресурсами персональные компьютеры, где приложения работают на пару слоев выше металла. Следовательно, организация вашего кода в шаблон имеет небольшую относительную стоимость. Для этой небольшой стоимости вы получаете следующие преимущества: проще добавлять сети, легко добавлять новые виды (регистраторы файлов, радары, HUD, несколько уровней масштабирования, ...), сохранить код модели «чище», отделив его от вида И контроллер, и я утверждаю, более читаемый код.

медиатор

Давайте рассмотрим бесконечный цикл while в последнем бите кода. Какова его работа? Он в основном отправляет сообщение Tick () в «Вид» и «Контроллер» так же быстро, как может управлять ЦП. В этом смысле его можно рассматривать как часть оборудования, отправляющего сообщения в программу, подобно клавиатуре; Его можно считать другим контроллером.

Возможно, если время «настенных часов» влияет на нашу игру, будет еще один контроллер, который отправляет сообщения каждую секунду, или, возможно, будет другой вид, который выплевывает текст в файл журнала. Теперь нам нужно подумать о том, как мы будем обрабатывать несколько видов и контроллеров. Это приводит нас к следующему образцу нашей архитектуры - Посреднику.

архитектура

Мы реализуем шаблон посредника, создавая объект EventManager. Этот посредник позволит уведомлять несколько слушателей, когда некоторые другие изменения объекта будут изменены. Кроме того, этот изменяющийся объект не должен знать, сколько слушателей есть, их можно даже добавить и удалить динамически. Все, что требуется изменить, это отправить событие EventManager при его изменении.

Если объект хочет прослушать события, он должен сначала зарегистрироваться в EventManager. Мы будем использовать weakref WeakKeyDictionary, чтобы слушатели не должны были явно отменить регистрацию. Мы также создадим класс Event для инкапсуляции событий, которые могут быть отправлены через EventManager.

class Event:
    """this is a superclass for any events that might be generated by an
    object and sent to the EventManager
    """
    def __init__(self):
        self.name = "Generic Event"

class EventManager:
    """this object is responsible for coordinating most communication
    between the Model, View, and Controller.
    """
    def __init__(self ):
        from weakref import WeakKeyDictionary
        self.listeners = WeakKeyDictionary()

    #----------------------------------------------------------------------
    def RegisterListener( self, listener ):
        self.listeners[ listener ] = 1

    #----------------------------------------------------------------------
    def UnregisterListener( self, listener ):
        if listener in self.listeners.keys():
            del self.listeners[ listener ]
        
    #----------------------------------------------------------------------
    def Post( self, event ):
        """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()

Переадресация: типы событий и выборочные прослушиватели

Поскольку мы получаем все больше и больше слушателей, мы можем обнаружить, что неэффективно спамить каждого слушателя с каждым событием. Возможно, некоторые слушатели только заботятся о некоторых событиях. Один из способов сделать вещи более эффективными - это классифицировать события в разных группах.

Для целей этого руководства мы просто используем один вид события, поэтому каждый слушатель получает спам в каждом событии.

Расширенные менеджеры событий

Если вы попытаетесь использовать этот конкретный класс Event Manager для своего собственного проекта, вы можете заметить, что у него есть некоторые недостатки. В частности, если блок кода генерирует события A и B последовательно, а слушатель захватывает событие A и генерирует событие C, вышеупомянутый класс Event Manager будет обрабатывать события в порядке A, C, B вместо желаемого порядка А, В, С. В более поздних примерах вы можете увидеть пример более продвинутого Event Manager, который всегда поставляет события в желаемом порядке.
Вот еще информация о шаблоне посредника и соответствующем шаблоне наблюдателя: Посредник @ Wikipedia Observer @ ootips.org

Игровая модель

Создавая модель, нам нужно пройти процесс, называемый абстракцией. Раньше мы играли в игры, поэтому у нас есть ценная ментальная библиотека конкретных примеров готовой продукции, аналогичной, в принципе, готовому продукту, который мы хотим создать. Если мы найдем абстрактные общие черты в этих конкретных примерах, это поможет нам создать классы для организации нашего кода, сделать его гибким, сделать его удобным и дать нам словарь для общения с другими членами команды о коде. Есть много возможных абстракций, которые мы можем придумать, и судить о том, создали ли мы хорошую абстракцию, очень субъективно. Важно помнить о своих целях, а также предвидеть, как в будущем могут измениться требования.

Вот модель, которая сработала для меня и является достаточно общей, чтобы адаптироваться ко многим типам игр:

Пример применения

Игра

Игра в основном представляет собой контейнерный объект. Он содержит Игроков и Карты. Он также может делать такие вещи, как Start () и Finish (), и отслеживать, чей это направление.

игрок

Объект Player представляет собой человека (или компьютер), который играет в игру. Общие атрибуты: Player.score и Player.color. Не путайте его с Charactor. Pac Man - это Charactor, человек, держащий джойстик, является игроком.

Charactor

А Charactor контролируется игроком, который перемещается по карте. Синонимы могут быть «Единица» или «Аватар». Это намеренно написано «Charactor», чтобы избежать какой-либо двусмысленности с Символом, который также может означать «одну букву» (также вы не можете создать таблицу в PostgreSQL с именем «Символ»). Атрибутами Common Charactor являются Charactor.health и Charactor.speed.

В нашем примере «маленький человек» станет нашим единственным «Характором».

карта

Карта - это область, в которой могут перемещаться Шаракторы. Обычно существуют два вида карт: дискретные, которые имеют Секторы, и непрерывные, имеющие местоположения. Шахматная доска - пример дискретной карты. 3-мерный уровень в Quake (с точностью с плавающей запятой) или уровень в Super Mario (с точностью до пикселя) являются примерами непрерывных карт.

В нашем примере Карта будет дискретной Картой, имеющей простой список из девяти секторов.

сектор

Сектор является частью Карты. Он находится рядом с другими секторами карты и может иметь список таких соседей. Никакой Charactor не может логически находиться между секторами. Если Charactor находится в секторе, он находится в этом секторе целиком, а не в каком-либо другом секторе (я говорю здесь функционально. Он может выглядеть так, как будто он находится между Sectors, но это проблема для View, а не Модель)

В нашем примере мы не допустим никаких диагональных движений, только вверх, вниз, влево и вправо. Каждый допустимый ход будет определяться списком соседей для конкретного сектора, причем средний сектор имеет все четыре.

Место нахождения

Мы не будем входить в локации непрерывной Карты, так как они не применяются к нашему примеру.

Пункт

Вы заметите, что на рисунке элемент явно не связан ни с чем. Это остается за разработчиком. У вас может быть конструктивное ограничение того, что элементы должны содержаться в Charactors (возможно, в промежуточном объекте «Inventory»), или, может быть, для вашей игры имеет смысл хранить список кусков предметов в объекте Game. Некоторые игры могут требовать, чтобы Секторы имели предметы, лежащие внутри них.

Наш пример

Пример применения

В этом примере используется все, что было рассмотрено до сих пор. Он начинается со списка возможных событий, затем мы определяем нашего посредника EventManager со всеми методами, которые мы показали ранее.

Затем у нас есть наши контроллеры, KeyboardController и CPUSpinnerController. Вы заметите, что нажатия клавиш больше не контролируют какой-либо игровой объект, а просто генерируют события, которые отправляются в EventManager. Таким образом, мы отделили Контроллер от Модели.

Затем у нас есть части наших PyGame View, SectorSprite, CharactorSprite и PygameView. Вы заметите, что SectorSprite сохраняет ссылку на объект Sector, часть нашей модели. Однако мы не хотим напрямую обращаться к каким-либо методам этого объекта сектора, мы просто используем его для определения того, какой объект сектора соответствует объекту SectorSprite. Если бы мы хотели сделать это ограничение более явным, мы могли бы использовать функцию id ().

В Pygame View есть фоновая группа зеленых квадратных спрайтов, которые представляют объекты сектора, и группу переднего плана, содержащую нашего «маленького человека» или «красную точку». Он обновляется на каждом TickEvent.

Наконец, мы имеем объекты Модели, как обсуждалось выше, и в конечном счете функцию main ().

Вот диаграмма основных входящих и исходящих событий.

Пример входящих сообщенийПример исходящих сообщений

ЧАСТЬ 2

Интернет-игра

Наша следующая задача - сделать игру игрой через Интернет. В конце концов, это приведет к многопользовательской игре для нашей игры, но важно, чтобы мы сначала выполнили однопользовательский сетевой шаг, так как он предоставляет нам несколько ограничений, которые могут повлиять на любой будущий код.

Код в следующих разделах записывается постепенно, поэтому не ожидайте просто взять код из первого раздела и написать игру с ним. В последующих разделах иногда рассматриваются проблемы с ранее показанным кодом и объясняются, как преодолеть эти проблемы.

Быстрое развитие

Одна из целей этого учебника - показать, как быстро можно развивать игру. Обычно что-то, связанное с сетью, является анафемой «быстрой», потому что, как только вы вводите сеть, вы вводите многопроцессорность, латентность, обработку ошибок и общее вытягивание волос. Принцип примеров кода в этом руководстве состоит в том, чтобы убедиться, что игра может быть запущена, даже не включив сеть. Сетевая функция должна иметь нулевой эффект на код в example.py - запуск игры в режиме одиночного игрока не должен выполнять какие-либо сетевые коды кода. Строго говоря об этом разделении, мы надеемся, что мы сможем быстро разработать игру , независимо от того, что может вызвать сетевой код.
Различные способы структурирования сетевых хостов

Структура сетевых хостов

Компьютерные процессы (обычно на каждом физическом компьютере или «хозяине» есть только один интересный процесс, поэтому мы просто используем термин «хост»), которые могут быть организованы по сети. В играх есть три популярных способа структурирования хостов, Peer-to-Peer, «Strict» Client-Server и «Servent» Client-Server. Главным фактором при определении того, как структурировать сетевые хосты для игр, как правило, является доверие. Игры эмоциональны (хорошо, хорошие) и конкурентоспособны, игроки мотивированы на победу, и когда они не выигрывают, они хотят верить, что ни у кого другого игрока не было несправедливого преимущества. Чтобы обеспечить доверие, должна быть последовательная, авторитетная модель.

В структуре «Строгий» клиент-сервер есть один «сторонний» сервер, к которому подключаются все клиенты. Любые изменения в авторитарной игровой модели должны происходить на сервере. Клиент может предсказать авторитетное состояние, но он не должен доверять игровому состоянию, пока он не услышит от сервера, что это действительно так. Пример игры будет World of Warcraft.

Клиент-сервер @ Википедия

В структуре Client-Server «Servent» один из игроков, обычно тот, который запускает игру, также выступает в качестве сервера. Это страдает от недостатка, что другие игроки доверяют игровому состоянию столько, сколько они доверяют конкретному игроку. Однако никакой третьей стороне не требуется. Примеры можно найти во многих играх шутера от первого лица. Эта структура часто сопрягается с сторонним «сопоставимым» сервером, который соединяет игроков друг с другом, а затем передает их на хост Servent.

Служебный @ Википедия

В структуре «равный-равному» все хосты имеют одинаковые роли. Большим преимуществом структуры Peer to Peer является то, что он надежно работает с отключением сети от отдельных хостов. Однако доверие скомпрометировано. Доверие можно укрепить, приняв стратегию передачи токена, так что хост, содержащий токен, действует как Servent.

Peer to Peer @ Википедия

В наших примерах мы рассмотрим структуру «Строгий» клиент-сервер.

Синхронный/асинхронный

Когда вы вызываете функцию, требующую сетевого общения, это может занять много времени. Если мы ждали, что функции, зависящие от сети, закончатся, прежде чем мы позвоним функциям, которые рисуют графику, пользователи разозлится и начнут плакать на интернет-досках объявлений. Решение состоит в том, чтобы писать функции, которые отправляют сообщения по сети, а затем немедленно возвращаются, не дожидаясь ответа. Ответы будут в конечном итоге поступать с удаленных хостов и ждать в очереди, пока наш процесс не сможет их обслуживать. Важно помнить, что ответы не могут стоять в очереди в том же порядке, что и запросы.

Это асинхронное качество является основополагающим для сетевого кода. К счастью, разработка нашего кода таким образом, что есть независимый EventManager, и четко определенные события сделают работу с асинхронными сообщениями из сети довольно безболезненными.

В этом учебнике будет использоваться структура Twisted для сетевого кода. Я рекомендую читать документацию Twisted, хотя не обязательно проходить этот урок. (Обратите внимание, что большая часть документации Twisted сосредоточена на написании серверов, где реализация клиента неизвестна. Я рекомендую перейти к разделам «Перспективные брокеры»). Представленные здесь идеи должны быть независимыми от выбора Twisted; Примеры могут быть также реализованы с использованием сырых сокетов или несущих голубей.

Twisted - это фреймворк, который скрывает от нас очередь, он ожидает, что программист вызовет реактор.run (), который является mainloop, который потребляет очередь и отключает обратные вызовы. Обратные вызовы предоставляются программистом.

Реализация

Пример сервера

Для сервера мы начнем с того же кода, что и раньше. Просто переименуйте example.py в server.py.

Обычно сервер - это то, что работает как демон или в текстовой консоли; Он не имеет графического дисплея. Мы можем сделать это просто, заменив PygameView на TextLogView следующим образом:

#------------------------------------------------------------------------------
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. Изменяя только небольшой код, у нас больше нет дисплея Pygame, вместо этого TextLogView просто выводит полученные события на консоль.

Еще одна вещь, которая нам не нужна на сервере, - это ввод с клавиатуры, поэтому мы можем удалить KeyboardController. Откуда берутся входные сообщения? Они поступают из сети, поэтому нам нужен объект Controller для сообщений, отправленных клиентами NetworkClientController.

from twisted.spread import pb
#------------------------------------------------------------------------------
class NetworkClientController(pb.Root):
        """..."""
        def __init__(self, evManager):
                self.evManager = evManager
                self.evManager.RegisterListener( self )

        #----------------------------------------------------------------------
        def remote_GameStartRequest(self):
                ev = GameStartRequest( )
                self.evManager.Post( ev )
                return 1

        #----------------------------------------------------------------------
        def remote_CharactorMoveRequest(self, direction):
                ev = CharactorMoveRequest( direction )
                self.evManager.Post( ev )
                return 1

        #----------------------------------------------------------------------
        def Notify(self, event):
                pass
Экземпляр NetworkClientController - это специальный объект, который может быть отправлен по сети через механизм перспективного брокера Twisted (поскольку он наследуется от pb.Root). Удаленный клиент запросит ссылку на экземпляр NetworkClientController, после его получения он может вызывать любой метод, начинающийся с «remote_». Поэтому для отправки клиентом сообщений на сервер мы реализовали remote_GameStartRequest и remote_CharactorMoveRequest.

Предостережение

Может возникнуть соблазн сделать все объекты удаленными ссылками. (Т. Е. Наследовать от pb.Referenceable) Проблема с этим подходом заключается в том, что он плотно соединяет сетевой код с остальной частью кода. Желательно отделить сетевой код, чтобы другие объекты использовали стратегию передачи событий, описанную в шаблоне посредника.

В наших примерах мы будем иметь только один класс на сервере, который является ссылочным, а также только один класс в клиенте. Нам также не нужен CPUSpinnerController на сервере, поэтому мы удалили его и заменили его реактором 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 для запуска игры, теперь нам нужно явно начать игру с нашего нового события GameStartRequest.

class GameStartRequest(Event):
        def __init__(self):
        self.name = "Game Start Request"
Нет необходимости понимать Twisted части этого, вы можете просто считать их «волшебными». То, что вы должны знать, заключается в том, что вызов реактора.run () заставляет mainloop блокироваться при прослушивании порта 8000.

Если мы сыграем некоторые грязные трюки, мы увидим, что делает наш сервер, не называя клиента. Вместо этого мы просто подключимся к нему с помощью интерактивного интерпретатора Python. Теперь реактор.run () является блокирующим вызовом, который не возвращается, пока реактор не выключится, поэтому, чтобы вернуться к интерактивной подсказке, нам нужно свернуть реактор и затем вызвать реактор.iterate (), чтобы Общайтесь с ним. Разумеется, это не рекомендуется. Кроме того, если вы повторите сеанс ниже, вам может потребоваться многократно вызывать iterate (), прежде чем вы увидите какой-либо результат.

  $ Python
 Python 2.5.2 (r252: 60911, 21 апреля 2008, 11:17:30) 
 [GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu7)] на linux2
 Введите «помощь», «авторское право», «кредиты» или «лицензия» для получения дополнительной информации.
 >>> from twisted.spread import pb
 >>> от реактора импорта twisted.internet
 >>> factory = pb.PBClientFactory ()
 >>> server = None
 >>> def gotServer (serv):
 ... глобальный сервер
 ... server = serv
 ... 
 >>> connection = reactor.connectTCP («localhost», 8000, заводская)
 >>> reactor.callLater (4, реактор.краш)
 <Twisted.internet.base.DelayedCall экземпляр на 0xac5638>
 >>> reactor.run ()
 >>> d = factory.getRootObject ()
 >>> d.addCallback (gotServer)
 <Отложенный по текущему результату 0xb1f440: Нет>
 >>> reactor.iterate ()
 >>> server.callRemote ('GameStartRequest')
 <Отложено на 0xac5638>
 >>> reactor.iterate ()
 >>> вверх, вправо, вниз, влево = 0,1,2,3
 >>> server.callRemote ('CharactorMoveRequest', вверх)
 <Отложено на 0xb1f4d0>
 >>> reactor.iterate ()
 >>> server.callRemote ('CharactorMoveRequest', справа)
 <Отложено на 0xac5638>
 >>> reactor.iterate ()
 >>> server.callRemote ('CharactorMoveRequest', вниз)
 <Отложено на 0xb1f4d0>
 >>> reactor.iterate ()
 >>> server.callRemote ('CharactorMoveRequest', слева)
 <Отложено на 0xac5638>
 >>> reactor.iterate ()
Пример использования консоли Python в качестве поддельного клиента
  $ Python server.py 
 Запрос на запуск игры
 Карта завершенного здания
 Начало игры
 Событие размещения Charactor в <__ main __. Экземпляр сектора в 0xc9b290>
 Запрос на перенос Charactor
 Запрос на перенос Charactor
 Событие перемещения Charactor до <__ main __. Экземпляр сектора в 0xc9b320>
 Запрос на перенос Charactor
 Событие перемещения Charactor до <__ main __. Экземпляр сектора в 0xc9b290>
 Запрос на перенос Charactor
 Событие перемещения Charactor до <__ main __. Экземпляр сектора в 0xc9b3b0>
Запуск server.py

Обратите внимание, что запрос на перемещение не привел к событию Move.

Мы можем подделать клиента более правильным способом, используя инструмент, который поставляется с Twisted, twisted.conch.stdio. Мы просто начинаем интерпретатор python с этим модулем, и тогда мы можем опустить злоупотребление реакторами:

  $ Python -m twisted.conch.stdio
 >>> from twisted.spread import pb
 >>> от реактора импорта twisted.internet
 >>> 
 >>> factory = pb.PBClientFactory ()
 >>> server = None
 >>> 
 >>> def gotServer (serv):
 ... глобальный сервер
 ... server = serv
 ... 
 >>> connection = reactor.connectTCP («localhost», 8000, заводская)
 >>> d = factory.getRootObject ()
 >>> d.addCallback (gotServer)
 <Отложен на 0xc227a0 текущий результат: Нет>
 >>> server.callRemote ('GameStartRequest')
 <Отложенные # 0>
 Отложенный # 0 перезвонил: 1
 >>> вверх, вправо, вниз, влево = 0,1,2,3
 >>> server.callRemote ('CharactorMoveRequest', вверх)
 <Отложенный # 1>
 Отложенный # 1 перезвонил: 1
 >>> server.callRemote ('CharactorMoveRequest', справа)
 <Отложенный # 2>
 Отложенный # 2 перезвонил: 1
 >>> server.callRemote ('CharactorMoveRequest', вниз)
 <Отложенные # 3>
 Отложенный # 3 перезвонил: 1
 >>> server.callRemote ('CharactorMoveRequest', слева)
 <Отложенный # 4>
 Отложенный # 4 перезвонил: 1
Использование twisted.conch.stdio в качестве поддельного клиента

Король замка

Как видно выше, объект реактора Twisted предназначен для управления основным контуром. Это создает определенные трудности, так как у нас уже есть основной цикл в CPUSpinnerController. Мы могли бы подчинить реактор Twisted и «нагнетать» его на каждую итерацию основного цикла CPUSpinnerController, но у этого есть недостаток, который нам нужно злоупотреблять API Twisted таким образом, который, вероятно, не был предназначен, и может быть не совместим с переходом.
# Example of a class that pumps a Twisted reactor
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
В качестве альтернативы мы можем использовать Twisted в соответствии с намеченным способом, а затем использовать класс LoopingCall, чтобы активировать событие Tick в зависимости от основного цикла реактора. Создание объекта LoopingCall - это способ попросить реактор повторно вызвать функцию. Недостатком такого подхода является то, что игры часто начинаются в однопользовательском режиме, и мы не хотим вызывать какой-либо сетевой код, например Twisted, если пользователь не выберет многопользовательский вариант.
# Example of using LoopingCall to fire the Tick event
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)
В конечном счете, выбор зависит от вас. Вы должны взвесить плюсы и минусы каждого подхода, основываясь на типе игры, которую вы пишете. В примерах мы будем использовать подход к перекачке реактора.

Сообщения за провода

Предыдущий пример сервера дал хорошее представление о базовой сетевой технике, но для наших целей это слишком просто. Мы не хотим писать новую функцию для каждого сообщения, которое сервер может получить. Вместо этого мы хотели бы использовать уже существующие классы событий.

Это приводит нас к одной из наиболее важных частей, но, возможно, к самой утомительной части внедрения сетей. Нам нужно пройти все возможные события и ответить на эти вопросы о каждом:

  1. Нужно ли отправлять его с клиента на сервер?
  2. Нужно ли отправлять его с сервера клиенту?
  3. Существуют ли проблемы безопасности при отправке этих данных по сети?
  4. Форматируются ли данные таким образом, что их можно отправить по сети?
  5. Если нужно, как мы переформатируем данные, чтобы их можно было отправить?
(В конце концов, можно также спросить: «Как часто будет отправляться это сообщение?» И, следовательно, «Как я могу лучше всего оптимизировать это сообщение?»)

Хотя есть много способов сделать это с помощью Twisted, я опишу стратегию, которая пытается свести к минимуму количество написанного кода (чтобы бороться с утомительностью этой задачи) и поддерживать разделение сетевых требований с остальной частью кода.

Используя Twisted, мы должны сделать три вещи для класса, чтобы дать возможность отправлять экземпляры по сети: наследовать его от twisted.spread.pb.Copyable, сделать его наследованием от twisted.spread.pb.RemoteCopy и вызвать Twisted.spread.pb.setUnjellyableForClass () на нем Все может стать еще более сложным, если мы рассмотрим вопросы 4 и 5 из нашего списка выше - нужны ли данные специального форматирования для отправки по сети? Единственными данными, которые не требуют специального форматирования, являются литеральные типы: string, int, float и т. Д., None и контейнеры (списки, кортежи, dicts).

При рассмотрении событий произойдет два случая: либо он не потребует переформатирования, и мы можем просто смешивать pb.Copyable и pb.RemoteCopy, или это потребует переформатирования, и нам нужно будет создать новый класс, который имеет рутину Изменить исходные данные на то, что может быть отправлено по сети. В следующем примере мы разделили код на несколько файлов. Все события находятся в events.py. В network.py мы пытаемся ответить на все вышеперечисленные вопросы для каждого события в events.py. Если сообщение отправляется от клиента на сервер, мы добавляем его в список clientToServerEvents, а также для списка serverToClientEvents. Если данные в событии просты, например, целые числа и строки, то мы можем просто смешивать классы pb.Copyable и pb.RemoteCopy и вызывать 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 )

On the other hand, if an event contains data that is not network-friendly, like an object, we need to make a replacement event to send over the wire instead of the original. The simplest way to make a replacement is just to change any event attributes that were objects to unique integers using the id() function. This strategy requires us to keep a registry of objects and their ID numbers, so that when we receive an event from the network referencing an object by its ID number, we can find the actual object.

# 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.

Создание канала связи

Мы видели, что мы можем послать поддельные сообщения на сервер с помощью интерактивной оболочки Python, но то, что мы действительно хотим это графический клиент. Есть несколько шагов для реализации этой цели. Во-первых, клиент (ы) должны быть уведомлены о любых изменениях в состоянии сервера. Таким образом, мы должны будем двунаправленная связь. Мало того, что клиент посылает запросы на сервер, а сервер также уведомляет клиента о событиях. (Вот почему в один конец ( «тянуть») протоколы, такие как 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 хранит ссылку реестра сервера, который отображает объект идентификационных номеров для реальных объектов. Она также имеет список клиентов. Объекты клиентов перечени наследуют от pb.Referenceable, поэтому мы можем использовать метод callRemote (), отправка сообщений по сети. Список serverToClientEvents импортируется из network.py.

NetworkClientView.Notify() is primarily interested in Copyable events. The event passed in to Notify() might already be Copyable, due to the mixing in of pb.Copyable in network.py. In that case, isinstance( ev, pb.Copyable ) returns True. If it's not Copyable, there still might be a replacement class in the network module, and we can check by prepending "Copyable" to the event's class name because we used that naming convention for the replacement classes in network.py.

As can be seen in NetworkClientView.Notify(), the server expects the client to send it a remotely accessible object (like one that inherits from Twisted's pb.Root) when the client connects. Thereafter, the server can use that object to notify the client of events.

Now we'll (finally) get started on the client. From the point of view of the client, the incoming messages from the server represent a Controller, so we've got a NetworkServerController class in client.py. As you might be expecting, the client will also send events to the server through a View, the 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, он пытается подключиться к серверу. Когда соединение выполнено, метод Connected () вызывается со ссылкой на объект сервера, который наследует от pb.Referenceable, поэтому клиент может использовать его для удаленного доступа к серверу. Он также создает 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 и использует его, чтобы передать серверу ссылку на самое себя. Теперь сервер может вызвать метод remote_ServerEvent () в NetworkServerController. Так как сервер и клиент имеют ссылки удаленно вызываемые объекты. Это канал, через который они общаются.
example applicaton

Локальная копия состояния сервера

Теоретически, должна быть только одна модель, авторитетная модель на сервере, и клиенты должны быть просто представления и контроллеры для этой модели. Тем не менее, это не простой вопрос , чтобы сохранить ссылки на удаленные объекты модели в клиента EventManager, представлений и контроллеров. Кроме того , многие события могут быть обработаны полностью на стороне клиента, и всегда посылать их на сервер будут создавать ненужный шум. Так что хороший дизайн должен создать локальную модель для отражения игровых объектов , которые существуют на стороне сервера.

We will create a PhonyModel on the client side whose state we will keep in sync with the authoritative model on the server. This PhonyModel provides the same interface as the server's model, but it has a special role - to ensure that the local game objects do not change the game state when they don't have the authority to do so. In our example, this is accomplished by keeping two EventManager objects, one called phonyEventManager, which just discards events that it receives, effectively silencing all events coming from the local game objects, and one called realEventManager, which propogates events received from the server. Events posted to the realEventManager will show up in the View objects, events posted to the phonyEventManager will not.

Because our example is very simple, we can get away with this simple implementation. One can imagine situations where we might want to allow a local game object to change the local state. This could be accomplished by making PhonyEventManager propogate these special events. Another approach could be to not have a local Model on the client, only a View object on which incoming events from the server had a direct effect.

Sending Complex Objects

Here's the tricky part: how do we send complex objects like Players or Charactors over the channel we've created? This is called serialization . To serialize our objects, we need to do two things.

Получение уникальных идентификаторов легко, можно использовать результат функции идентификатора () при вызове на объекте в вопросе на сервере . Она должна исходить от сервера , так что он является уникальным, в противном случае мы должны были бы несколько идентификаторов для одного объекта.

Когда события ссылающихся сложных объектов добраться до NetworkClientView на сервере, объекты сериализуются, начиная с конструктора 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 в качестве примера. Приведенный выше код будет вызывать __init __ () для 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
Как вы можете видеть, сервер не будет посылать реальный объект, когда он отправляет событие, он будет посылать только уникальный целочисленный идентификатор. Это также гарантирует, что существует отображение из этого идентификатора реального объекта в реестре.

Когда клиент послал CopyableCharactorMoveEvent, то PhonyModel поднимает его вверх (PhonyModel единственный объект заинтересован в событиях, которые начинаются с «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 движется, он находится в новом секторе. Для того, чтобы сообщить об этом клиенту, сервер отправляет CharactorMoveEvent, который имеет один атрибут, сам Charactor. Клиент получает это событие, видит Charactor ссылается внутри, и просит новое состояние (какой сектор он в?) Для этого Charactor.

Это очень общий подход к решению проблемы.

Этот разговор 4 сообщения. Это могло бы быть короче; мы могли бы иметь только ручную работу с CopyableCharactorMoveEvent в чем-то более конкретные потребности нашей игры, к примеру, мы могли бы включить этот сектор в качестве атрибута события устранени запрос для получения дополнительной информации.Но мы будем держать код очень родовым сейчас. Много других событий будут следовать той же схеме.

Back to the code snippet, if the client has already received that object from the server, self.sharedObjs.has_key() will return true, and it can grab a reference to the object from the registry and carry on as normal. If it hasn't received that object yet (as is the case the first time this event is received), it must first create a placeholder object, and then copy the state of the object on the server into this new placeholder object. It does this by calling GetObjectState() with the unique ID of the needed object.

GetObjectState() basically just finds that object on the server (in this example, the Charactor that has moved), and serializes it's data with a call to getStateToCopy(). GetObjectState() returns the dict and the object ID that was requested.

# 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]
ДИКТ что getStateToCopy() возвращает содержит все сетевые чистые данные, поэтому он может быть отправлен по сети.

Клиент получает эту информацию в функции StateReturned(), который, вероятно, является самой трудной функцией следовать во всем этом учебнике. Я буду стараться, чтобы пройти через этот шаг за шагом.

Клиент сначала запрашивает состояние объекта. Когда придет ответ, обратные вызовы StateReturned и CharactorMoveCallback выстраиваются в очередь, чтобы называться в последовательности.

# from client.py

        def Notify(self, event):
                ...

                        remoteResponse = self.server.callRemote("GetObjectState", charactorID)
                        remoteResponse.addCallback(self.StateReturned)
                        remoteResponse.addCallback(self.CharactorMoveCallback)
Первый обратный вызов, StateReturned будет вызываться с [ObjectId, objDict] в качестве своего «ответа» аргумент.
# 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:
                        #to complete the current object, we need to grab the
                        #state from some more objects on the server.  The IDs
                        #for those needed objects were passed back
                        #in 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 is a Deferred - returning it causes a chain
                        # to be formed.
                        return retval
В простейшем случае «успеха» истинно, и GetAllNeededObjects () немедленно возвращает None. Тогда следующий обратный вызов, CharactorMoveCallback вызывается, и мы сделали.

However, if "success" was False, that means more data is needed to complete the originally requested object's state. The PhonyModel keeps a list of neededObjects that must be requested from the server before the originally requested object is complete. Each of these needed objects may also append to the neededObjects list for subsequent objects they need. So when we call GetAllNeededObjects() the recursive behaviour begins.

# from client.py

        #----------------------------------------------------------------------
        def GetAllNeededObjects(self):
                if len(self.neededObjects) == 0:
                        #this is the recursion-ending condition.  If there are
                        #no more objects needed to be grabbed from the server
                        #then we can try to setCopyableState on them again and
                        #we should now have all the needed objects, ensuring
                        #that setCopyableState succeeds
                        return self.ConsumeWaitingObjectStack()

                #still in the recursion step.  Try to get the object state for
                #the objectID on the top of the stack.  Note that the recursion
                #is done via a deferred, which may be confusing 
                nextID = self.neededObjects[-1]
                remoteResponse = self.server.callRemote("GetObjectState",nextID)
                remoteResponse.addCallback(self.StateReturned)
                return remoteResponse

As you can see, another call is made to GetObjectState on the server that will result in StateReturned being called. Notice that this isn't truly recursive. GetAllNeededObjects doesn't block. It returns immediately. But it returns a Deferred object, remoteResponse. So the original Deferred had it's first callback called, and that returned a new Deferred object. This is called Chaining Deferreds and it causes the first callback to block until the second Deferred's callbacks are finished. Hence we get recursion over the network.

Here is a flowchart that summarizes the actions taken when the client gets an event containing a complex object.

flowchart of client event reception

Notice that we must make sure that the event we send over the network has enough information to update the client with any relevant changes to the state of the server. The client may already have a local version of an object, but if that object has changed , the client still has to call GetObjectState(), as is demonstrated with the CharactorMoveEvent.

With that in mind, a question is raised: where do we put the intelligence do determine what object states we need to retrieve? Right now, we've put all this logic in PhonyModel.Notify() [TODO: is this the best place? what about inside Copyable events?]

More Problems

The previous discussion is a good start and provides some useful code. I encourage you to play around with it and see if you can get your game sending objects back and forth. As your code becomes more complex, you will run into some more problems:

  1. What if we don't have enough information to call __init__ for some attributes in setCopyableState() ?
  2. What if we don't know the specific subclass for an attribute in setCopyableState() ?

To clarify, here's an example of when an issue like this might come up. Lets say we write a game where two Penguins fight each other. Each Penguin has a weapon, and every weapon is initialized with a name, like "Deathbringer" or "Destroy-o-Matic", or "Daffodil".

#------------------------------------------------------------------------------
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
Мы наивно начать писать соответствующую функцию setCopyableState:
    def setCopyableState(self, stateDict, registry):
        neededObjIDs = []
        success = 1

        wID = stateDict['weapon']
        if not registry.has_key( wID ):
            #registry didn't have the object, so create a new one
            self.weapon = Weapon( self.evManager,
            #WELL CRAP!  I don't yet know what its name is,
            so how am I going to initialize it?
Кроме того, позволяет сказать, что Оружие одного из трех подклассов, либо Slingshot, винтовка, или Nuke. Тогда мы еще больше трудностей с setCopyableState:
        ...
        wID = stateDict['weapon']
        if not registry.has_key( wID ):
            #registry didn't have the object, so create a new one
            self.weapon = ???
            #MORE CRAP!  I don't even know what class of object
            it should be!

Мы можем решить эту проблему с помощью объекта заполнителя , который очень похож на Ленивом Proxy шаблон проектирования.

... Мультиплеер

Несетевой Мультиплеер

Мы начнем с создания игры на 2-х игроков, который работает локально, а не по сети. Наша слабосвязанная архитектура позволяет нам делать это, и это большое преимущество, чтобы иметь возможность развивать свои идеи первыми и беспокоиться о проблемах сети позже.
example applicatonexample applicaton