Многопользовательский онлайн-шутер на WebGL и asyncio, часть вторая


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

    Введение
    1. Структура игры
    2. Импорт моделей и blender
    3. Подгрузка моделей в игре с babylon.js и сами модели
    4. Передвижения, миникарта и звуки игры в babylon.js
    5. Вебсокеты и синхронизация игры
    6. Игроки и их координация
    7. Балансировка игроков по комнатам и объектный питон
    8. Asyncio и генерация поведения бота
    9. Nginx и проксирование сокетов
    10. Асинхронное кэширование через memcache
    11. Послесловие и RoadMap

    Всех кому интересна тема асинхронных приложений в Python3, WebGL и просто игр, прошу под кат.

    Введение


    Сама статья задумывалась как продолжение темы о написании асинхронных приложений с использованием aiohttp и asyncio, и если первая часть была посвящена тому, как лучше сделать модульную структуру по типу django, поверх aiohttp( asyncio ), то во второй уже захотелось заняться чем то более творческим.

    Естественно, мне показалось интересным сделать 3D-игрушку, а где еще как не в играх может понадобится асинхронное программирование.

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

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

    Но следует понимать, что это не полноценная игра — в том смысле, что написал git clone и пошёл в банкомат. Это, скорее, попытка сделать игровой фреймворк, демку возможностей asyncio и webgl в одном флаконе. Здесь нет витрины, рейтингов, досконально оттестированной безопасности и т.д., но, с другой стороны, мне кажется, что для open sourse проекта, разрабатываемого от случая к случаю, в свободное время, достаточно нормально получилось.

    2 Импорт моделей и blender


    Естественно, нам для игрушки нужны 3D модели персонажей, нужны модели для имитации ландшафта, строений и т.д.
    Персонажами могут быть люди, танки, самолеты. Есть два варианта — нарисовать модель, и импортировать уже готовую. Самый простой способ, это найти готовые модели на одном из специализированных сайтов, например тут или тут. Кому интересен процесс рисования танков и прочих моделей, на ютубе полно видео, на хабре встречаются материалы по этой тематике.

    Остановимся подробней на самом процессе импорта. Из импортируемых в blender форматов, чаще всего встречаются .die .obj .3ds.

    У импорта/экспорта есть ряд нюансов. Например, если мы импортируем .3ds, то, как правило, модель импортируется без текстур, но с уже созданными материалами. В таком случае нам просто надо каждому материалу загрузить с диска текстуры. Для .obj, как правило, кроме текстур должен идти .mtl файлик, если он присутствует, то обычно вероятность возникновения проблем меньше.

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

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

    Для решения этой задачи существует два способа:
    1) Объединить все детали модели и сделать её одним объектом. Этот способ немного быстрее, но он работает только если у нас есть одна текстура в виде UV — развертки. Для этого можно через аутлайнер с шифтом выделить все объекты, они подсветятся характерным оранжевым цветом и в меню object выбрать пункт join.


    2) Следующий вариант — связать все детали по принципу Родитель-Ребенок. В таком случае с текстурами проблем не будет, даже если на каждую деталь у нас своя текстура. Для этого нужно правой кнопкой мыши выделить поочередно родительский объект и ребенка, нажать ctrl+P выбрать в меню object. В результате в аутлайнере мы должны увидеть, что все объекты, из которых состоит наша модель, относятся к одному родителю.


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

    3. Подгрузка моделей в игре с babylon.js и сами модели


    Сама загрузка моделей выглядит достаточно просто, указываем местонахождение меша на диске:

    loader = new BABYLON.AssetsManager(scene);
    mesh  = loader.addMeshTask('enemy1', "", "/static/game/t3/", "t.babylon");

    После этого мы в любом месте можем проверить что наша модель уже загружена и дальше выставить её позицию и преобразовать, если нужно.

    mesh.onSuccess = function (task) {
          task.loadedMeshes[0].position = new BABYLON.Vector3(10, 2, 20);
     };

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

    var palm = loader.addMeshTask('palm', "", "/static/game/g6/", "untitled.babylon");
    palm.onSuccess = function (task) {
            var p = task.loadedMeshes[0];
            p.position   = new BABYLON.Vector3(25, -2, 25);
            var p1 = p.clone('p1');
            p1.position = new BABYLON.Vector3(10, -2, 20);
            var p2 = p.clone('p2');
            p2.position = new BABYLON.Vector3(15, -2, 30);
    };

    Также клонирование играет важную роль в случае с AssetsManager. Он рисует простенькую заставку, пока не подгрузится основная часть сцены, и следит чтоб загрузилось все что мы помещаем в loader.onFinish.

    var createScene = function () {
        .  .  .
    }
    var scene = createScene();
    loader.onFinish = function (tasks) {
        engine.runRenderLoop(function () {
            scene.render();
        });
    };


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


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

    Персонажи, в данном случае, у нас представлены двумя типами танков, Т-90 и Абрамс. Поскольку игровой логики побед и поражений у нас сейчас нет, и в случае с фреймворком подразумевается, что все это нужно в каждом отдельном случае придумывать. Поэтому сейчас нет выбора и первое лицо всегда играет Абрамсом, а бот и все остальные игроки видны как Т-90.

    На самой карте мы видим некоторый рельеф, он создается очень просто, с помощью возможности babylon.js под названием heightMap, для этого нужно на текстуру грунта наложить черно белую картинку и в зависимости от того, где светлая поверхность, а где темная и формируются горы и холмы, часть характеристик можно указать в параметрах, чем более размытый переход между темным и белым, тем более пологий склон.

        var ground = BABYLON.Mesh.CreateGroundFromHeightMap("ground", "/static/HMap.png", 200, 200, 70, 0, 10, scene, false);
        var groundMaterial = new BABYLON.StandardMaterial("ground", scene);
        groundMaterial.diffuseTexture = new BABYLON.Texture("/static/ground.jpg", scene);
        ground.material = groundMaterial;


    Далее у нас есть небольшой антураж в виде дома, водонапорной башни возле него, нескольких деревьев и немного травы.
    Трава вышла у нас максимально низкополигональной, просто плоскостью с текстурой на ней. И эта плоскость наклонирована по разным местам. Вообще, чем более низко полигональные модели, тем лучше, с точки зрения производительности, но по понятным причинам будет страдать зрелищность.
    Конечно, можно сделать возможность выбора в настройках "качества графики", но не в нашем случае.
    В отличии от травы, банановая пальма у нас имеет достаточно много вершин, поэтому было решено оставить только пару штук на карте.
    Чем больше вершин на карте, тем ниже может быть FPS и тд.


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

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


    4. Передвижения, миникарта и звуки игры в babylon.js


    Говоря о передвижениях, в основном речь идет о первом лице, так как передвижения других игроков и ботов — это всегда просто смена позиции, которая чаще всего транслируется с помощью сокетов с сервера, о чем будет написано ниже.

    Само по себе движение — это просто управление камерой и видимой частью игрока. Например, игрок поворачивает вправо, значит мы должны повернуть камеру направо относительно того, куда сейчас смотрим, или провернуть сцену на нужный градус. Также и модель, изображающая, например, какое-то средство поражения оппонентов, также должна повернутся.
    В основном в играх, сделанных на babylon.js для передвижения от первого лица, существуют две камеры:

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

    Примеры:

    //FollowCamera
    var camera = new BABYLON.FollowCamera("camera1", new BABYLON.Vector3(0, 2, 0), scene);
    camera.target = mesh;
    ``````javascript
    //FreeCamera
    var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3( 0, 2, 0), scene);  
    mesh.parent = camera;

    Во время движения должны быть какие-то звуки, во время попадания, во время выстрела, во время движения и т.д. У babylon.js есть хорошее API управления звуком. Тема управления звуком довольно обширна, поэтому мы рассмотрим только пару небольших примеров.
    Пример инициализации:

    var music = new BABYLON.Sound("Music", "music.wav", scene, null, {
              playbackRate:0.5,  // скорость воспроизведения. 
              volume: 0.1,           // громкость воспроизведения.
              loop: true,               // указывает нужно повторять или нет звук.
              autoplay: true         // указывает нужно ли сразу запускать проигрывание.
    });

    Пример звука выстрела — при клике проверяем, что поинтерлок снят и воспроизводим звук одиночного выстрела.

    var gunS = new BABYLON.Sound("gunshot", "static/gun.wav", scene);
    window.addEventListener("mousedown", function (e) { 
          if (!lock && e.button === 0) gunS.play(); 
    });

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

        var ms = new BABYLON.Sound("mss", "static/move.mp3", scene, null, { loop: true, autoplay: false });    
        document.addEventListener("keydown",  function(e){
            switch (e.keyCode) {
               case 38: case 40: case 83: case 87:
            if (!ms.isPlaying) ms.play();
            break;
            }
        });
        document.addEventListener("keyup",   function(e){
            switch (e.keyCode) {
                case 38:  case 40: case 83: case 87:
                    if (ms.isPlaying) ms.pause();
                    break;
                   }
        });

    Миникарта


    У любого шутера должна быть миникарта на которой видно либо только своих игроков, либо всех сразу, видно строения и общий ландшафт, в нашем случае, если приглядеться, видно и снаряды. В babylon.js есть несколько способов это реализовать. Наверное, самый простой — это создать еще одну камеру, установить её сверху над картой и разместить вид с этой камеры в нужном нам углу.


    В нашем случае мы берем freeCamera и говорим ей, что она должна размещаться сверху:

    camera2 = new BABYLON.FreeCamera("minimap", new BABYLON.Vector3(0,170,0), scene);

    Чем больше координата по Y, тем полнее обзор, но тем мельче детали на карте.
    Далее говорим камере как будем размещать на экране изображение с неё.

    camera2.viewport = new BABYLON.Viewport(x, y, width, height);

    И последнее — надо добавить на сцену обе камеры (когда камера одна не обязательно это делать):

    scene.activeCameras.push(camera);
    scene.activeCameras.push(camera2);

    5. Вебсокеты и синхронизация игры


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

    Изначально, поскольку мы пользуемся FreeCamera, то она является родительским объектом, следовательно, мы используем её координаты. Например:

    • camera.cameraRotation — содержит X и Y координаты поворота по осям.
    • camera.position — содержит X, Y и Z координаты местонахождения меша на карте.

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



    6. Устройство серверной части


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

    async def game_handler(request):
        .  .  .
        async for msg in ws:
            if msg.tp == MsgType.text:
                 if msg.data == 'close':
                     await ws.close()
                 else:
                     e = json.loads( msg.data )
                     action = e['e']
                     if action in handlers:
                         handler = handlers[action]
                         handler(ws, e)
                         .  .  .

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

    def h_move(me, e):
        me.player.set_pos(e['x'], e['y'], e['z'])
        mess = dict(e="move", id=me.player.id, **me.player.pos_as_dict)
        me.player.room.send_all(mess, except_=(me.player,))

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

    class Player(list):
        .  .  .
        def __init__(self, client, room, x=0, y=0, z=0, a=0, b=0):
            list.__init__(self, (x, y, z, a, b))
            self._client = client
            self._room = room
            Player.last_id += 1
            self._id = Player.last_id
            room.add_player(self)
    
        .  .  .
        def set_rot(self, a, b): self[3:5] = a, b
    
        def getX(self): return self[0]
        .  .  .
        def setX(self, newX): self[0] = newX
        .  .  .
        x = property(getX, setX)
        .  .  .
        @property
        def pos_as_dict(self):
            return dict(zip(('x', 'y', 'z'), self.pos))
    

    В классе Player мы используем property, для более удобной работы с координатами. Кому интересно, на хабре был хорший материал на эту тему.

    7. Балансировка игроков по комнатам


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

    На данный момент вся информация о том, какой игрок в какой комнате и т.д., хранится в памяти, поскольку, раз у нас нет еще витрины и рейтингов, то нет и смысла использовать какую-то базу.

    Номер комнаты присваивается игроку когда он со стартовой странички, которая находится по роуту /pregame, заходит в игру. При нажатии на кнопку, срабатывает ajax, который в случае удачного результата переадресовывает игрока в нужную комнату.

    if (data.result == 'ok') {
        window.location = '/game#'+data.room;

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

    def check_room(request):    
        found = None
        for _id, room in rooms.items():
            if len(room.players) < 3:
                found = _id
                break
        else:
            while not found:
                _id = uuid4().hex[:3]
                if _id not in rooms: found = _id

    За работу с комнатами у нас отвечает класс Room. Его общая схема работы выглядит примерно так:


    Тут мы видим, что он взаимодействует с классом Player, возможно, вся схема выглядит не совсем линейно, но в конечном итоге она позволит довольно удобно писать такие вот цепочки:

    # разослать сообщение всем игрокам находящимся в комнате 
    me.player.room.send_all( {"e" : "move",  .  .  . })
    # получить всех игроков комнаты
    me.player.room.players
    # добавить игрока в комнату
    me.player.room.add_player(self)
    # удалить игрока из комнаты
    me.player.room.remove_player( me.player )

    Немного хотелось бы поговорить по поводу me.player, поскольку у некоторых из коллег это вызвало вопросы, me это сокет который передается как параметр в функциях, которые обслуживают события:

    def h_new(me, e):
        me.player = Player(me, Room.get( room_id ), x, z)

    Тут, на самом деле, как сказал бы переводчик, игра слов. Поскольку мы знаем, что все в питоне есть объект.
    Вот так происходящее будет наглядней:

    player = Player( Room.get(room_id), x, z)
    player.me = me
    me.player = player

    И у нас получаются две ссылки, player модуля и .player объекта me, обе равноправны и ссылаются на один и тот же объект в памяти, который будет существовать пока есть хоть одна ссылка.

    Это можно увидеть на еще более простом примере:

    >>> a = {1}
    >>> b = a
    >>> b.add(2)
    >>> b
    {1, 2}
    >>> a
    {1, 2}

    В этом примере b и a — это просто ссылки на одно общее значение.

    >>> a.add(3)
    >>> a
    {1, 2, 3}
    >>> b
    {1, 2, 3}

    Смотрим далее:

    >>> class A(object):
    ...     pass
    ... 
    >>> a = A()
    >>> a.player = b
    >>> a.player
    {1, 2, 3}
    >>> b
    {1, 2, 3}
    >>> a.__dict__
    {'player': {1, 2, 3}}

    Свойства объектов — это просто синтаксический сахар. В данном случае они просто сохраняются в словарь __dict__

    В итоге мы сейчас убили одну нашу ссылку a, но вместо неё создали другую, принадлежащую новосозданному объекту, а на самом деле лежащую в словаре __dict__ этого объекта.

    >>> a
    <__main__.A object at 0x7f3040db91d0>

    8. Asyncio и генерация поведения бота


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

    mess = dict(e="move", bot=1, id=self.id, **self.pos_as_dict)
    self.room.send_all(mess, except_=(self,))

    Общая схема работы бота и его взаимодействия с клиентской частью выглядит следующим образом:


    В классе Room мы в __init__ создаем экземпляр класса Bot. А уже в def __init__ в самом классе Bot мы в asyncio.async(self.update()) передаём задачу, которая должна выполняться на каждом проходе.
    Вызов функции, содержащей await не запускает саму функцию, а создаёт объект-генератор. Так же как и вызов функции, объявленной как class не запускает эту функцию, а создаёт объект, обслуживаемый этим классом. Вызов функции, содержащей await произойдёт тогда, когда у генератора будет вызван метод .__next__(). В данном случае — next находится в декораторе async — он инициализирует корутину.
    Проще говоря, каждые 100 миллисекунд мы отсылаем клиенту сообщение с новыми координатами для бота, и каждые полсекунды у нас обновляются координаты бота.

    Простой пример работы с задачами в бесконечном цикле:

    import asyncio
    
    async def test( name ):
        ctr = 0
        while True:
            await asyncio.sleep(2)
            ctr += 1
            print("Task {}: test({})".format( ctr, name ))
    
    asyncio.ensure_future( test("A") )
    asyncio.ensure_future( test("B") )
    asyncio.ensure_future( test("C") )
    
    loop = asyncio.get_event_loop()
    loop.run_forever( )

    Все функции, которые мы помещаем в asyncio.ensure_future, будут выполнятся по кругу с задержкой, указанной в asyncio.sleep(2) в две секунды). Практическое применение у этого очень обширное, помимо ботов для игр можно писать просто боты для систем трейдинга, к примеру, и, что удобно, не запуская для этого, например, отдельные скрипты. Что на мой субъективный взгляд упрощает разработку и местами, что очень ценно, позволяет избежать зоопарка.

    9. Nginx и проксирование сокетов


    И последнее что стоит упомянуть в связи с игрой — это правильную настройку Nginx для случаев когда мы точно знаем, что наш проект будет работать и с websocket и с http. Первое, что приходит в голову, это примерно такая конфигурация:

    server {
            server_name        aio.dev;
             location / {
                     proxy_pass http://127.0.0.1:8080;
             }
    }

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

    server {
            server_name        aio.dev;
             location / {
                     proxy_pass http://5.5.0.10:8080;
             }
    }

    Но она тоже ущербна, потому что производительность питона ниже производительности nginx на порядки, и в любом случае proxy_pass должен быть http://127.0.0.1:8080
    Поэтому воспользуемся возможностью Nginx-a, появившейся пару лет назад, проксированием сокетов:

    server {
            server_name        aio.dev;
             location / {
                     proxy_pass http://127.0.0.1:8080;
             }
            location /ws {
                  proxy_pass http://127.0.0.1:8080;
                  proxy_http_version 1.1;
                  proxy_set_header Upgrade $http_upgrade;
                  proxy_set_header Connection "upgrade";
           }
    }

    Для адреса при инициализации сокетов мы указываем порт 80 var uri = "ws://aio.dev:80/ws", потому что Nginx по умолчанию настроен на прослушивание 80 порта, если мы явно не указываем listen.
    И в такой конфигурации все будет работать за Nginx-ом, и будут удобно доступны и websoket-ы, и http.

    10. Асинхронное кэширование.


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

    В этом качестве рассмотрим пример кэширования страниц с помощью memcached для функций с async def. В любом более-менее посещаемом классическом сайте должна быть возможность закэшировать посещаемые страницы, например — главную, страницу новости, и т.д., а также возможность сбросить кеш при изменении страницы, до истечения времени кэширования. Благо асинхронный драйвер для memcached уже был написан svetlov, оставалось написать декоратор и решить пару небольших проблем.

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

    Сериализоваться данные для помещения их в memcached будут с помощью pickle. И еще один нюанс — поскольку фреймворк написан поверх aiohttp, то в нем не сериализовался CIMultiDict, это реализация словаря с возможностью иметь одинаковые ключи, и написанная для большей скорости на Cython автором aiohttp.

        dct = CIMultiDict()
        print( dct )
        <CIMultiDict {}>
        dct = MultiDict({'1':['www', 333]})
        print( dct )
        <MultiDict {'1': ['www', 333]}>
        dct = MultiDict([('a', 'b'), ('a', 'c')])
        print( dct )
        <MultiDict {'a': 'b', 'a': 'c'}>
        dct = dict([('a', 'b'), ('a', 'c')])
        print( dct )
        {'a': 'c'}

    Поэтому те значения, которые хранились в нем, оказались не сериализуемы в pickle. Поэтому пришлось их достать и перепаковать обратно, но надеюсь, что со временем CIMultiDict станет сериализуемым в pickle.

    d = MultiDict([('a', 'b'), ('a', 'c')])
    prepared = [(k, v) for k, v in d.items()]
    saved = pickle.dumps(prepared)
    restored = pickle.loads(saved)
    refined = MultiDict( restored )

    Полный код кэширования
    def cache(name, expire=0):
        def decorator(func):
            async def wrapper(request=None, **kwargs):
                args = [r for r in [request] if isinstance(r, aiohttp.web_reqrep.Request)]
                key = cache_key(name, kwargs)
    
                mc = request.app.mc
                value = await mc.get(key)
                if value is None:
                    value = await func(*args, **kwargs)
                    v_h = {}
                    if isinstance(value, web.Response):
                        v_h = value._headers
                        value._headers = [(k, v) for k, v in value._headers.items()]
                    await mc.set(key, pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL), exptime=expire)
                    if isinstance(value, web.Response):
                        value._headers = v_h
                else:
                    value = pickle.loads(value)
                    if isinstance(value, web.Response):
                        value._headers = CIMultiDict(value._headers)
                return value
    
            return wrapper
    
        return decorator


    Применить кэширование можно просто, написав сверху декоратор и указав там время истечения кэша и название.

    from core.union import cache
    
    @cache('list_cached', expire=10 )
    async def list_tags(request):        
        return templ('list_tags', request, {})

    Послесловие


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

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

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

    Ping

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

    Читерство

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

    Roadmap — игры


    Client:


    • Стрельба у бота, больше видов оружия.
    • Управление танком — мышью поворачивается только башня, стрелочками сам танк
    • Передвижение под разными углами
    • Добавление карт и управления для войны в космосе/небе
    • Добавление карт и персонажей пехоты для войны в поле like Urban Terror

    Server:


    • Проверка всех передвижений
    • Проверка направлений выстрела
    • Расширение возможностей ботов ( например, увеличение количества ботов, динамическое их убывание и т.д.)
    • Базовый ИИ у ботов

    Roadmap — фреймворка в целом


    • Небольшая CMS
    • Конструктор для складского учета (мини ERP)
    • Конструктор отчетов
    • Web клиент для MongoDB
    • Демка мини соц-сети

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

    Первая часть
    Обзорная статья по babylon.js и сравнение его с three.js
    Библиотека на github
    Документация на readthedocs
    Один из основных сайтов с большим выбором платных и бесплатных 3D Моделей
    Сайт с бесплатными 3D моделями для Blender
    Ротация
    работа со звуком в babylon.js
    Пример визуализации звука
    Нello world на asynio
    Sleep
    Работа с задачами
    Отстроченные вызовы
    pep-0492
    Блог svetlov автора aiohttp
    Асинхронный драйвер memcached
    Обновленная документация для babylon.js
    Документация по aiohttp на github
    Документация по aiohttp на readthedocs
    Документация по yield from
    aio-libs — список библиотек
    Еще один более полный список

    Более подробно про генераторы:
    http://www.dabeaz.com/generators/Generators.pdf
    http://www.dabeaz.com/coroutines/Coroutines.pdf
    http://www.dabeaz.com/finalgenerator/FinalGenerator.pdf
    • +26
    • 20.6k
    • 5
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 5

      0
      На заметку: «таски» на 3.5+ отлично работают через await без уродских yield from.
        +1
        Занудства ради, на 3.5+ не только «таски» работают через await.
        –3
        Самое интересное начнется, если вы из прототипа будете делать игру. Внезапно окажется, что игровой логики станет столько, что при онлайне в 50 человек одного ядра процессора перестанет хватать, и захочется не только оторвать бизнес-логику от обработчиков вебсокетов, но и разделить ее на несколько серверов. Вот тогда будет настоящее бэкенд-проектирование.
          +1
          Вот это уже интересно. А есть примеры проектов, распределяющих бизнес-логику одной локации на несколько серверов? Было бы интересно почитать как они с этим живут.

          Мне кажется здесь скорее оптимизация уйдёт в недра Cython, либо иных низкоуровневых решений, свободых от GIL, чем в распределённую среду, которую проектировать и обслуживать сразу на порядок сложнее. Обычно онлайн игры стремятся наоборот изолировать наименьшее количество взаимодействующих игроков на отдельном шарде.
          0
          Как планируется высчитывать «попадание»?
          Для каждой пули на карте каждые 20ms высчитвается новое положение, и проверяется есть ли пересечение с танком чтоб нанести урон, либо есть ли пересечение с внешним миром чтоб пулю уничтожить.?

          Only users with full accounts can post comments. Log in, please.