Багатокористувацький онлайн-шутер на 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
, то яку дію зробив гравець і у відповідності з цим викликаємо потрібну функцію, обробник події:

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 in action 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, in room 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( {"е" : "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())
передаємо завдання, яка повинна виконуватися на кожному проході.
Виклик функції, яка містить
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("С") )

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
має
<a href="http://127.0.0.1"></a>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( down )

Повний код кешування
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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.