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

в 8:06, , рубрики: aiohttp, asyncio, babylon.js, game development, Gamedev, html, javascript, mongodb, nginx, nosql, python, python3, WebGL, игры

Многопользовательский онлайн-шутер на WebGL и asyncio, часть вторая - 1
В этом материале постарался описать создание браузерного 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.

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

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

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();
    });
};

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

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

Персонажи, в данном случае, у нас представлены двумя типами танков, Т-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;

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

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

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

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

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 есть несколько способов это реализовать. Наверное, самый простой — это создать еще одну камеру, установить её сверху над картой и разместить вид с этой камеры в нужном нам углу.

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

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

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

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

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

def game_handler(request):
    .  .  .
    while True:
        msg = yield from ws.receive()
        try:
            if msg.tp == MsgType.text:
                if msg.data == 'close':
                    close(ws)
                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. Его общая схема работы выглядит примерно так:

Многопользовательский онлайн-шутер на WebGL и asyncio, часть вторая - 11
Тут мы видим, что он взаимодействует с классом 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,))

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

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

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

import asyncio

@asyncio.coroutine
def test( name ):
    ctr = 0
    while True:
        yield from asyncio.sleep(2)
        ctr += 1
        print("Task {}: test({})".format( ctr, name ))

asyncio.async( test("A") )
asyncio.async( test("B") )
asyncio.async( test("C") )

loop = asyncio.get_event_loop()
loop.run_forever( )

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

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 должен быть 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 для функций с @asyncio.coroutine. В любом более-менее посещаемом классическом сайте должна быть возможность закэшировать посещаемые страницы, например — главную, страницу новости, и т.д., а также возможность сбросить кеш при изменении страницы, до истечения времени кэширования. Благо асинхронный драйвер для 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):
        @asyncio.coroutine
        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 = yield from mc.get(key)
            if value is None:
                value = yield from 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()]
                yield from 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 )
@asyncio.coroutine
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

Автор: Alex10

Источник


  1. MHzSerge:

    Огромное спасибо за статью!!!

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js