Танчики на node.js — оптимизация

    Спасибо всем, кто пытался поиграть в первый раз. Очень жаль, что я разочаровал столько людей жуткими тормозами игры. Но я мог бы и не догадаться до их причины, если бы не вы. Сейчас сервер порядком оптимизирован, но количество одновременных игр увеличено всего до пяти. Это незначительно, но дело уже не в производительности сервера, а в том, что в худшие вечерние часы скорость моего интернета не позволит больше. Заманухи ради появилась возможность выбрать уровень перед стартом игры. А также в ответ на «обидный» комментарий, появилась возможность поиграть 2 на 2. Итак — демка, альтернативный сервер, еще сервер. Сейчас остается надеяться, что я не сильно поспешил, и сервер не подведет. Под катом я расскажу, каких глупостей наделал в первой версии.

    Профилирование


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

    node --prof src/server

    По завершении скрипта в текущей папке появится файл v8.log. Чтобы его превратить во что-то удобоваримое, я воспользовался утилитой linux-tick-processor из исходников v8. Не вдавался в подробности работы linux-tick-processor, но, чтобы получить её себе и заставить работать, пришлось выполнить несколько команд:

    svn co http://v8.googlecode.com/svn/branches/bleeding_edge/ v8
    cd v8
    svn co http://gyp.googlecode.com/svn/trunk build/gyp
    make native

    После make native в текущей папке появится папка out с бинарниками, которыми пользуется linux-tick-processor. Чтобы обработать v8.log, в папке v8 выполняем:

    tools/linux-tick-processor /full/path/to/v8.log > /full/path/to/v8.out

    В получившемся v8.out смотрим результаты. Инфа о профилировании взята отсюда. Если я делаю это слишком сложно и кто-то знает способ лучше, буду рад узнать.

    Имитируем нагрузку


    Второй раз испытывать судьбу и рушить надежды сотен хабралюдей поиграть в танчики мне не захотелось. И я решил имитировать нагрузку на сервер своими силами. Для этого неплохо подходит Selenium, a точнее Selenium Server. Если кто-то не знаком, Selenium Server — это java приложение, способное запускать практически любой браузер, и выполнять разнообразные команды: кликать по ссылкам, нажимать кнопки, проверять наличие или отсутствие определенного текста на странице, и многое другое. Так же реализованы клиенты для многих языков программирования.
    Для своих целей я написал небольшой php-скрипт, который открывает страницу, авторизуется, запускает игру и ждет 30 секунд. Запустив этот скрипт на выполнения в нескольких консолях:

    phpunit --repeat=10 test.php

    получаем неплохой способ имитировать нагрузку.

    Оптимизация 1 — замыкания


    Запускаем node с профайлером, обрабатываем лог, и смотрим v8.out. В файле v8.out функции отсортированы по убыванию времени исполнения. Первой в логе идет функция Array.forEach:

    ticks  total  nonlib   name
    1514    2.3%   14.9%  LazyCompile: *forEach native array.js:1019

    Тут я не сразу понял в чем дело. Я решил, что это из-за того, что в некоторых местах я использую конструкцию:

    someArray.forEach(function(item){
        ...
    }, this);

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

    Class.prototype.handler = function(item)
    {
       ...
    }
    
    Class.prototype.func = function()
    {
        someArray.forEach(this.handler, this);
    }

    Такой код должен быстрее выполняться, но как выяснилось позже, дело было не в этом.

    Оптимизация 2 — поиск пересечений


    Запускаем профайлер, запускаем selenium тесты и смотрим дальше:

       ticks  total  nonlib   name
      31043    3.8%   16.5%  LazyCompile: MapArrayed.intersects src/utils/map_arrayed.js:45
      26763    3.3%   14.3%  Stub: CEntryStub
      22800    2.8%   12.1%  LazyCompile: *forEach native array.js:1019
      16323    2.0%    8.7%  LazyCompile: IN native runtime.js:353
      13800    1.7%    7.4%  Stub: KeyedLoadElementStub
       6911    0.9%    3.7%  LazyCompile: *MapArrayed.forEach src/utils/map_arrayed.js:59

    Что ж, не удивительно. Поиск пересечений занимает слишком много времени, нужно переписать карту. Я описывал три способа в прошлой статье: трехмерный массив с индексами-координатами, список прямоугольников и дерево вложенных прямоугольников. Поигравшись с деревом я пришел к выводу, что при 1000 объектов на карте большого выигрыша в производительности не получается. Идея трехмерного массива с индексами-координатам мне не нравится в принципе. Я остановился на следующем способе: в основе его так же остались объекты заданные прямоугольниками, но чтобы каждый раз не пробегать по всем объектам карты, объекты сгруппированы по небольшим ячейкам. То есть получаем тот же трехмерный массив, но с другим смыслом. Первые две размерности — это индексы ячеек, по сути это те же координаты, только с грубым приближением. Третья размерность — список объектов-прямоугольников пересекающихся с данной ячейкой. Отличия от предложения Oblitus в том, что такой способ не накладывает никаких ограничений на минимальный шаг перемещения для объектов и зернистость такого массива можно безболезненно варьировать по своему усмотрению. Вполне возможно, что это все еще не оптимальный вариант.

    Оптимизация 3 — forEach


    Опять смотрим результаты профилирования. Тут получается странная картина, если я поиграл только в одну игру, то имеем:

      ticks  total  nonlib   name
        51    0.1%   11.6%  LazyCompile: *Loggable.log battlecity/src/server/loggable.js:32
        12    0.0%    2.7%  LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:106
         8    0.0%    1.8%  Stub: CEntryStub
         6    0.0%    1.4%  LazyCompile: *forEach native array.js:1019
         4    0.0%    0.9%  Stub: StringAddStub
         4    0.0%    0.9%  Stub: ArgumentsAccessStub_NewNonStrictFast

    Если я запустил selenium и сервер проиграл несколько десятков игр:

       ticks  total  nonlib   name
       4108    2.0%   16.1%  LazyCompile: *forEach native array.js:1019
       3626    1.8%   14.3%  Stub: CEntryStub
       2176    1.1%    8.6%  LazyCompile: *MapTiled.forEach battlecity/src/utils/map_tiled.js:139
       2048    1.0%    8.0%  LazyCompile: IN native runtime.js:353
       1755    0.9%    6.9%  Stub: KeyedLoadElementStub {1}
       1475    0.7%    5.8%  LazyCompile: *Loggable.log battlecity/src/server/loggable.js:32
        337    0.2%    1.3%  LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:106
        336    0.2%    1.3%  Stub: ToBooleanStub_Bool

    Странная вещь, со временем forEach() жрет все больше и больше времени. Скажу, что каждый объект на карте имеет свой уникальный id, и этот id глобальный, то есть со временем он только увеличивается. Но кол-во объектов-то на карте не меняется от парии к парии. Это не очень удачный прием, можно сделать id объекта локальным для каждого игрового поля, чтобы id не увеличивался бесконечно, но ведь в javascript массивы хранятся не как в C++. В javascript это должно быть что-то типа хеш-таблицы, так почему же время forEach все больше и больше? В этом месте я долго бился головой об стену, пока не догадался провести такой эксперимент:

    a=[1];
    a[1000000]=1;
    console.time('qwe');
    a.forEach(function(i){console.log(i);})
    console.timeEnd('qwe');
    console.time('asd');
    for (var i in a) console.log(a[i]);
    console.timeEnd('asd');

    В результате получаем неутешительные результаты.

    FF:
    qwe: 163ms
    asd: 2ms

    Chrome:
    qwe: 254ms
    asd: 1ms

    ну и Opera для общей картины:
    qwe: 0ms (188µsec)
    asd: 0ms (87µsec)

    Как видим в Опере forEach() и for(var i in ...) принципиально не отличаются по времени выполнения, но Chrome и Firefox очень сильно меня расстроили, именно по этому сервер (да и клиент) начинал сильно тормозить через несколько игр. Делать нечего, переписываем forEach() на for(var i in ...). И, о, чудо! Тормоза, которые я списывал на утечки памяти, пропадают!

    Оставляем node на пару часов запустив в нескольких консолях «phpunit --repeat=100 test.php» и видим:

       ticks  total  nonlib   name
        746    0.2%   16.1%  LazyCompile: *Loggable.log battlecity/src/server/loggable.js:28
        128    0.0%    2.8%  LazyCompile: *Game._stepItem battlecity/src/core/game.js:77
        101    0.0%    2.2%  LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:102
         61    0.0%    1.3%  Stub: CEntryStub
         52    0.0%    1.1%  Function: EventEmitter.emit events.js:38
         50    0.0%    1.1%  Stub: SubStringStub
         46    0.0%    1.0%  LazyCompile: *MapTiled.add battlecity/src/utils/map_tiled.js:24
         45    0.0%    1.0%  LazyCompile: FILTER_KEY native runtime.js:398

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

    Оптимизация 4 — трафик


    Тут я решил немного отступить от профайлера. Дело в том, что в поисках предыдущей оптимизации я посчитал трафик между клиентом и сервером. Оказалось в разгар игры клиенту может отправляться до 30кб/c. Очевидно, для такой игры, как танчики — это запредельная цифра. Но на это есть несколько причин. Во-первых, при изменения всего одного свойства, объект отправляется клиенту целиком. Во-вторых, объекты отправляются в JSON, что также значительно увеличивает размер передаваемых данных. Первоначально объекты отправлялись примерно так:
    Bullet.prototype.serialize = function()
    {
        return {
            type: 'Bullet',
            id: this.id,
            x: this.x,
            y: this.y,
            z: this.z,
            speedX: this.speedX,
            speedY: this.speedY,
            finalX: this.finalX,
            finalY: this.finalY
        };
    };

    что вело к передаче строки {«type»:«Bullet»,«id»:777,«x»:123,«y»:456,«z»:1,«speedX»:2,«speedY»:0,«finalX»:123,«finalY»:456} длинной около 100 байт. Немного подумав, переделал сериализацию объектов так, чтобы получался не объект, а массив:
    Bullet.prototype.serialize = function()
    {
        return [
            battleCityTypesSerialize['Bullet'], // 0
            this.id, // 1
            this.x, // 2
            this.y, // 3
            this.speedX, // 4
            this.speedY, // 5
            this.finalX, // 6 todo remove
            this.finalY // 7 todo remove
        ];
    };

    в результате получаем около 25 байт [0,777,123,456,2,0,123,456]. Трафик снизился примерно до 7-8кб/c в разгар игры. Его можно снизить еще в несколько раз, передавая только измененные свойства и передавая только управляющие команды, но эту переделку я оставил на будущее.

    Оптимизация 5 — синхронизация с клиентом


    Алгоритм синхронизации из прошлой статьи оказался неудачным. Единственная причина именно такого выбора, а не мгновенная рассылка изменений всем клиентам, которые в них заинтересованы, была в том, что вновь подключившийся клиент может получить все изменения в прошлом точно таким же способом, что и текущие данные. Так же во время реализации этого способа я пришел к идее сгруппировать объекты в коллекции, и обращаться за обновлениями не к самим объектам, а к коллекциям объектов. Такими коллекциями стали «все пользователи», «сообщения в общем чате», «список игр», «пользователи в текущей игре», «сообщения в текущей игре» и «объекты на карте текущей игры».
    Новым способом синхронизации стала немедленная рассылка изменений по объектам User, где они накапливаются. А объект User по прежнему отправляет данные браузеру пакетами, раз в 50мс. Остается вопрос когда и как синхронизировать первоначальные данные? Я решил добавить объекту User 2 метода: watchCollection() и unwatсhCollection() и в момент подключения к группе объектов, User отправляет клиенту все объекты, как вновь созданные:

    /**
     * @param collection
     * @param syncKey ключ, по которому клиент узнает к какой группе относятся объекты
     */
    ServerUser.prototype.watchCollection = function(collection, syncKey)
    {
        this.unwatchCollection(syncKey);
        // сюда будут складываться обновления объектов
        this.updateCollector[syncKey] = [];
        var user = this;
        var cb = function(item, type) {
            user.onCollectionUpdate(syncKey, item, type);
        };
        // подписываемся на обновления
        collection.on('update', cb);
        // запоминаем callback, чтобы при отключении от группы, можно было удалить обработчик
        this.collections[syncKey] = {'callback': cb, 'collection': collection};
        // отправляем клиенту все объекты группы, как вновь созданные
        collection.traversal(function(item){
            this.onCollectionUpdate(syncKey, item, 'add');
        }, this);
    };
    
    ServerUser.prototype.unwatchCollection = function(syncKey)
    {
        if (this.collections[syncKey]) {
            // удаляем обработчик
            this.collections[syncKey].collection.removeListener('update', this.collections[syncKey].callback);
            // сообщаем клиенту, чтобы он удалил у себя эту группу
            this.clientMessage('clearCollection', syncKey);
            delete this.collections[syncKey];
            delete this.updateCollector[syncKey];
        }
    };

    Таким образом сразу после авторизации пользователя на сервере, объект User подключается к трем группам объектов (коллекциям):
    user.watchCollection(registry.users, 'users');
    user.watchCollection(registry.premades, 'premades');
    user.watchCollection(registry.messages, 'messages');

    А во время входа в игру и выхода из игры, пользователь соответственно подключается и отключается от других интересующих его коллекций:
    
    Premade.prototype.join = function(user, clanId)
    {
        // ...
        user.watchCollection(this.users, 'premade.users');
        user.watchCollection(this.messages, 'premade.messages');
        // ...
    };
    
    Premade.prototype.unjoin = function(user)
    {
        // ...
        user.unwatchCollection('premade.users');
        user.unwatchCollection('premade.messages');
        user.unwatchCollection('f');
        user.unwatchCollection('game.botStack');
        // ...
    };
    
    Premade.prototype.startGame = function()
    {
        // ...
        this.users.traversal(function(user){
            // ...
            user.watchCollection(this.game.field, 'f');
            user.watchCollection(user.clan.enemiesClan.botStack, 'game.botStack');
            // ...
        }, this);
        // ...
    }
    

    Жаль, что сразу не приходят в голову простые и правильные мысли.
    В результате имеем:
        ticks  total  nonlib   name
       2074    0.5%    9.9%  LazyCompile: *Game._stepItem battlecity/src/core/game.js:29
        751    0.2%    3.6%  LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:102
        489    0.1%    2.3%  LazyCompile: MapTiled.traversal battlecity/src/utils/map_tiled.js:132
        376    0.1%    1.8%  LazyCompile: FILTER_KEY native runtime.js:398

    Судя по тому, что Game._stepItem вышел на первое место, и стал выполняться 9.9% времени, а не 2.8%, как раньше, считаем переделку успешной. На этот момент, сервер загружен примерно на 50% при 10 одновременных играх. Поставить 20 игр одновременно в демке я не рискнул, из-за того, что в худшие вечерние часы, скорость моего интернета падает до 200кБайт/с и ниже.

    Оптимизация 6 — обход игровых объектов


    Первоначально я не задумывался об этом, и для каждого объекта на поле в цикле вызывал функцию _stepItem():
    
    Game.prototype._stepItem = function(item)
    {
        // tanks and Base processing within Clan.step
        if (item.step && !(item instanceof Tank) && !(item instanceof Base)) { // todo
            item.step();
        }
    };
    
    Game.prototype.step = function()
    {
        this.field.traversal(this._stepItem, this);
        this.premade.clans[0].step();
        this.premade.clans[1].step();
    };
    


    Эта функция вызывается для всех кусочков стены, да и еще и проверяет прототип каждого объекта. Чтобы избавиться от этого безобразия, я завел массив stepableItems, который меняется при добавлении и удалении объектов с карты. И не нужно больше делать сложные проверки в часто вызываемой функции:
    
    Game = function Game(level, premade)
    {
        // ...
        this.stepableItems = [];
        this.field.on('add', this.onAddObject.bind(this));
        this.field.on('remove', this.onRemoveObject.bind(this));
        // ...
    };
    
    Game.prototype.onAddObject = function(object) {
        if (object.step && !(object instanceof Tank) && !(object instanceof Base)) {
            this.stepableItems[object.id] = object;
        }
    };
    
    Game.prototype.onRemoveObject = function(object) {
        delete this.stepableItems[object.id];
    };
    
    Game.prototype.step = function()
    {
        for (var i in this.stepableItems) {
            this.stepableItems[i].step();
        }
        // ...
    };
    


    В результате я вернулся опять к пересечению объектов, но уже на совсем другом уровне:
    
       ticks  total  nonlib   name
        129    0.0%    2.4%  LazyCompile: MapTiled.intersects battlecity/src/utils/map_tiled.js:102
         66    0.0%    1.2%  Stub: SubStringStub
         54    0.0%    1.0%  Stub: CEntryStub
         47    0.0%    0.9%  Function: EventEmitter.emit events.js:38
         39    0.0%    0.7%  LazyCompile: MapTiled.add battlecity/src/utils/map_tiled.js:24
         30    0.0%    0.6%  Function: Socket._writeOut net.js:389
    


    Теперь 5 игр одновременно занимают 15-16% процессорного времени. То есть мой старенький сервер должен потянуть около 30 игр в одном потоке.

    Бага с наследованием


    Пришлось побороться с одной багой, причины которой заслуживают внимания. При наследовании я забыл вызвать родительский «конструктор»:
    function Parent()
    {
        this.property = [];
    }
    
    function Child()
    {
        // Parent.apply(this, argiments); - забыл
    };
    
    Child.prototype = new Parent();
    Child.prototype.constructor = Child;

    В результате массив property оказался только в prototype, а не в this. То есть расшарен между всеми экземплярами Child, что не вызвало никаких ошибок во время выполнения, но привело к труднообнаружимому багу. Все таки с javascript ничего не стоит прострелить себе ногу.

    Планы на будущее


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

    Первая часть
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 56

      +3
      никто не комментирует, все играют?
        +1
        Никто не может поиграть, идёт бесконечная загрузка страницы… Все её ждут =)
          0
          пока-что сервер не загружается больше чем на 40%. Бесконечная загрузка, может быть из-за того, что вы за прокси рубящей websocket траффик. я не знаю как потестить эту ситуацию, у меня то сервер под рукой. поэтому внятного сообщения об ошибке не могу пока сделать. Как вариант можно на странице websocket.org/echo.html проверить работу websocket. Хотя если тесты на websocket.org работают, это еще не значит что между моим сервером и вами нету такой прокси.
            0
            У меня последний стабильный фаерфокс (8.0).
            Очень странно, что он не поддерживает веб-сокеты.
            Желательно написать где-то в шапке, что демка работает только под WebKit'ом.
              0
              я проверяю наличие вебсокетов. если их нет, то будет сообщение об этом. а вот если websocket есть, а подключиться через прокси не удается — то возможно бесконечно «подключение...» висеть будет.
                0
                Я точно не за прокси. + прямо сейчас с вами общаюсь через хром в чате =)
                  0
                  раз так, значит мое интернет подключение — узкое место.
          0
          занято…
          0
          Когда выбираю 2х2. Игра начинается, через 10 сек выкидывает в чат, потом снова сама начинается, и так без остановки
            0
            Оптимизировали, оптимизировали, да переоптимизировали.
              0
              Так вот откуда прирост, одновременного обслуживания :-)
              Автор быстрее чините, завтра пятница
              0
              тут две причины: или вы начинаете игру когда еще не набралось игроков на две команды, то есть три и более игроков. или вы наткнулись на баг, о котором я еще не знаю. скорее всего первое.
                0
                сама игра стартовать не должна, возможно ее стартует другой игрок из этого же premade.
                0
                Увеличьте лимит! И да, прощай рабочий день ;)
                  0
                  лимит боюсь увеличивать, лучше уж сколько есть без тормозов поиграют (возможно без тормозов).
                  0
                  А что нибудь с распаралеливанием всего этого дела не думали?
                    0
                    пока нет
                      0
                      надо сделать чтобы вход в игру 2на2 был таким:
                      1 игрок — снизу
                      2 игрок — сверху
                      3 игрок — снизу
                      4 игрок — сверху

                      сейчас
                      1 — снизу
                      2 — снизу
                      3 — сверху
                      4 — сверху
                        0
                        как только доберусь до этого, сделаю чтобы можно было менять команду, пока игра не началась.
                    +1
                    А какая версия node.js используется, если не секрет?

                    Ситуация с .forEach очень странная, думаю, можно оформить ее как баг v8 (Можно где-нибудь посмотреть код вашего сервера?). Строго говоря for ( ... in ...) должен приводить к деоптимизации всей функции, в которой он находится, так что странно, что все работало медленнее.

                    Если вы используете версию node.js 0.6.2 — пробовали запускать приложение с --trace-bailout, --trace-deopt флагами?
                      0
                      node — 0.4.12. Насчет флагов просвещусь. Но это не только v8, firefox также себя ведет.
                      «for (… in ...) должен приводить к деоптимизации всей функции» очень интересно понять, о чем вы говорите :) можете поделиться ссылками, где можно прочитать про внутренности v8?
                    • UFO just landed and posted this here
                        0
                        Как обычно, все зависит только от вашего кода, по-идее функция передаваемая в forEach должна инлайниться, и время выполнения будет практически таким же как при обычном цикле. Зато при использовании for (… in… ) вы гарантированно откажетесь от оптимизации всей функции, в которой он находится.
                        • UFO just landed and posted this here
                            0
                            Должна инлайниться — в данном случае, применимо только в компиляции (а если точнее — я имел в виду v8 ).

                            Я согласен с автором — в большинстве случаев все так и происходит, но в данной статье и конкретно в моем комментарии — речь только о node.js и v8.
                            • UFO just landed and posted this here
                                0
                                forEach() в первой версии использовался и на сервере и на клиенте. браузеры тоже сильно тормозили тогда. поэтому forEach() -> for(var i in ...) — это оказалась и серверная и клиентская оптимизация. так как node.js и chrome используют один js движок, то тесты в chrome так же относятся к node.js. Хотя судя по словам donnerjack13589 — это может быть просто бага и в будущем будет лучше вернуть все на forEach().
                                  0
                                  Это не бага, просто массив это структура, в которой численные ключи генерируются из порядкового номера значения. Когда вы записываете значение по ключу и используете его вроде как хеш, но не массив, вы и приводите к таким ситуациям. Это кстати свойственно не только JavaScript. А суть вся скорее всего в том, что внутри forEach() (который создан для обхода массивов, а значит 0..n) логика завязана на length, а как всем известно:
                                  var a = [];
                                  a[10000000] = 1;
                                  a.length; // 10000001
                                    0
                                    ну да, но ведь в javascript вроде как все объекты — хеш-таблицы, в том числе Array. и for(var i in ) хорошо справляется с любыми индексами, почему бы forEach() не работать так же? Тем более forEach в Опере с этим справляется.

                                    Хотя если подумать, там где я использую Array, как разреженный массив, можно просто использовать Object. Но тогда из вариантов обхода у меня остается только вариант с for(var i in ).
                                      0
                                      Привет. Решил перечитать старые топы по ноде:) как писалось выше:
                                      var a=[]
                                      a[10]=1
                                      a//=>[undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1]
                                      

                                      Смотрим спецификацию как должен работать forEach и как оно выглядит в коде, эмуляция от мозилы.
                                      a.hasOwnProperty(10)//=>true
                                      a.hasOwnProperty(9) //=>false
                                      

                                      Итого, вас будет 1000000 раз в цикле проверено наличие свойства у массива по индексу в цикле и два раза вызван калбэк. Все по спецификации. Просто у вас массивы такие:)
                                      Если есть желание перебирать такие массивы в функциональной форме через for(… in ...) — пожалуйста, дополните прототип Array чем-то типа:
                                      Array.prototype.forIn=function(cb,cont){
                                          for(var i in this)
                                              if(this.hasOwnProperty(i))cb.call(cont||this,this[i]);}
                                      

                                      Получаем:
                                      a=[1];
                                      a[1000000]=1;
                                      console.time('qwe');
                                      a.forEach(function(i){console.log(i);})
                                      console.timeEnd('qwe');                //=>404ms
                                      console.time('asd');
                                      a.forIn(console.log,console);
                                      console.timeEnd('asd');                //=>1ms
                                      

                                      Правда если писать в прототип так, а не через defineProperty с enumerable:false, а оно в старых ie недоступно, проблематично будет просто for(… in ...) использовать:)
                      0
                      Поиграл, но с большим трудом (тормозит слишком), хотя причиной может быть плохое соединение на рабочем месте.
                      Автор молодец.
                        0
                        Так может кто-нибудь выделит место на сервере с широким каналом под игру? Чтобы больше играющих было.
                        Автору уважение и почёт за воплощение детской мечты! Танчики с мультиплеером по сети! Играть очень весело!
                          +1
                          Я уже ниже написал, что готов :)
                          0
                          UI вас очень подкачал. После ввода ника (тут всё понятно), редиректит на отчаянно криво-косо свёрстанную страничку чата с нечитаемыми никами в левом столбце (из-за горизонтальных скроллов). И совсем не сразу понятно, что возможность поиграть прячется в правом верхнем углу. Дизайн, вёрстка, UI — желательно улучшить, чтобы было «не стыдно показать» простым пользователям, которым технические детали реализации самой игры глубоко до лампочки.

                          з.ы. максимум одновременных игр на сервере тоже маловат, ажиотаж создали — а поиграть не дали
                            0
                            Согласен по всем пунктам. Но сам я далек от дизайна. И realtime приложения совсем не мой конек. Вот втягиваюсь, так сказать. Думаю со временем все будет посимпатичнее.
                            0
                            Извините, но ваш браузер не поддерживает websocket. Рекомендуемые браузеры — Google Chrome версии 14 и выше, и Firefox версии 7 и выше.

                            Почему не работает в Safari на OS X Lion?

                            Более тупого определения WebSocket я не видел. Вы перебираете все браузеры на возможность в скрипте проверки? Не проще сделать что-то вроде этого:
                            if(typeof WebSocket == «undefined» || typeof MozWebSocket == «undefined») {alert(errMsg)}
                              0
                              Поменял User-Agent на Google Chrome под Windows XP — все заработало!!!
                              0
                              Супер!
                                0
                                а вы хоть видели как я проверяю наличие WebSocket?
                                смотрите как это тупо — code.google.com/p/battle-city-js/source/browse/src/client.js#84 :)
                                  +1
                                  Ой, извини… Посмотрел!
                                  В Safari typeof WebSocket возвращает «object».
                                  Добавте тогда в релиз условие проверки!
                                    0
                                    спасибо за подсказку. тогда да, лучше проверять на undefined. исправлю.
                                  0
                                  Только у меня танчик не стреляет и не ездит, а только крутится? :(
                                    0
                                    несколько человек говорили об этом в чате. скажите какой у вас браузер и ось? может получится изловить багу.
                                      0
                                      FF 7.0.1
                                      Ubuntu 11.10
                                    +2
                                    Огромная просьба — повесьте танчики на 80й порт.
                                    У многих посетителей (как, к примеру, у меня) этот порт закрыт на firewall'ах.
                                    А ещё лучше — слушайте ещё и на 443м порту.
                                      0
                                      в планах, к сожалению времени мало на танчики.
                                      0
                                      Ну, прогресс уже есть :)
                                      Поиграть смог, правда каждые 3-5 секунд все останавливается, потом бежит дальше.

                                      Если хотите, могу разместить у себя на сервере, который всеравно простаивает (дц в москве, мощный вдс) — пишите, если интересно
                                        0
                                        в результате получаем около 25 байт [0,777,123,456,2,0,123,456]

                                        Какой ужас. Разве нельзя никак передавать бинарные данные? Выигрыш был на порядок, без преувеличений
                                          0
                                          согласен, ужас. я пока не знаю, как передавать бинарные данные через socket.io.
                                        0
                                        open source?

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