Ни слова о луке

сэр шаман рассказывает о чём может

LimeJS: Пишем кроссплатформенную игру на HTML5 с поддержкой прикосновений

LimeJS - 2D Open Source HTML5 движок для написания игр с поддержкой прикосновений и работающий (по описанию) на большинстве мобильных платформ. Я наткнулся на него не сам, мне прислали письмо с просьбой рассказать о нём сообществу и я решил, раз так - что уж мелочиться, надо попробовать его в деле. Кроме того, я заранее договорился с авторами движка, что буду честен - буду рассказывать и о достоинствах и о недостатках, так что надеюсь убрать из статьи ореол рекламы (хотя какая реклама может быть связана с open source)..?

Open Source, кроссплатформенность и HTML5 - это то, что я люблю - инновации и свобода :). И ещё, сам движок написан на Closure и поддерживает chaining, это вносит дополнительные яркие цвета в свойства движка и программирование с его использованием. Конечно, необходимо ещё и удобство разработки игр само по себе, на что мы и испытаем LimeJS вместе с вами в этой статье. Движок преподносится как кроссплатформенный, на iPad'е представленные на сайте игры вполне себе работают, немного медленно, но вполне играбельно, ну а на моём Hero/Android2.1 (HTML5, наверное, неполный) они естественно подтормаживают и глючат - то есть буквально, играть в эти игры нельзя. Впрочем, практически все объекты в играх даже на смартфоне отображаются и действуют корректно, так что будем надеяться что с последующуей оптимизацией всё будет отлично даже на хилых смартфонах типа моего.

Движок, кстати, позиционируется как замена Flash-технологий в играх. Это болезненная тема для многих среди нас в связи с общим гноблением флэша, но при этом существующими и даже создающимися на нём отличными играми. (И, как я лично считаю, удобство самого механизма создания анимации в Flash пока ещё не повторено ни для HTML5/SVG ни для альтернатив). Так вот, может быть у этого движка действительно есть шанс завоевать любовь разработчиков на Flash и привить им любовь к HTML5. Решать им и вам. Главное отличие LimeJS от, допустим, ProcessingJS - ориентировка не на машину состояний, не на обновление в каждом кадре, а на “таймлайн” - событийность в сценарии игры.

Кстати, вот пример кода: javascript и html - чтобы вы могли сразу сделать какой-то вывод, а то я изначально относился к движку довольно скептически, а вот сейчас думаю, что наверняка зря.

Что получится

В течении прочтения статьи мы напишем очень упрощённую версию пинг-понга на LimeJS. Вот так будет выглядеть результат:

Мужчины в синих шортах на футбольном поле с детским мячиком

В конце статьи видео с демонстрацией написанной игры на iPad, iPhone и Android.

Подготовка к разработке

У движка есть небольшой CLI, Command Line Interface. Он написан на Python и скачивает нужные пакеты с помощью git, поэтому для работы с движком нужно установить Python, git и git-svn соответственно, если вдруг они не установлены (разработчикам с Windows видимо придётся помучиться). Затем берём исходники из github или скачиваем zip и распаковываем. На Ubuntu это будет выглядеть примерно так:

$ sudo apt-get install python git-core git-svn
$ wget https://github.com/digitalfruit/limejs/zipball/master -no-check-certificate
$ unzip ./master ./digitalfruit-limejs
$ cd ./digitalfruit-limejs

Чтобы автоматически установить другие нужные для разработки пакеты (включая Closure), запускаем:

$ ./bin/lime.py init

Начинаем наш проект

$ ./bin/lime.py create pingpong

Да, пусть это будет пинг-понг, подобный тому, который показывает Dominic в руководстве по созданию игры на Impact HTML5 Engine. Я потом обнаружил, что в демо-исходниках есть что-то похожее, но пусть у нас будет намного более простой вариант.

В каталоге pingpong будут созданы файлы pingpong.html и pingpong.js. Откройте .html файл в браузере, он уже довольно интересен - в центре вы увидите симпатичный круг, который можно таскать по странице мышкой или пальцем. В .js-файле тоже много полезного - показано как создаётся сцена и видно, как организовывается слежение за событиями. Код остаётся при этом вполне понятным и читаемым. Я не буду разбирать его подробно, это всё-таки просто пример-заглушка, а ссылки по которым можно посмотреть его “нутро” я привёл в начале статьи.

Основные классы и концепции

Краткое резюме Programming guide:

  • Director - это режиссёр игры, он управляет переходами между сценами (включая анимацию переходов) и содержит основные настройки игры;
  • Scene - это сцена, отдельный экран в игре, на него добавляются дочерние объекты и слои;
  • Layer - это слой, участки экрана удобно разделять/распределять на слои и слои тоже могут быть контейнерами дочерних объектов. При этом они вполне могут перекрываться, как в фотошопе;
  • ScheduleManager - планировщик, помогает запускать определённые функции либо в каждом кадре, либо по прошествию указанного времени;
  • Node - любая сущность в игре, имеет свою позицию, локальную систему координат и размер, может перемещаться, вращаться, масштабироваться и анимироваться;
  • Sprite - наследник Node, имеет все его свойства/способности и может представлять собой изображение и/или геометрический объект (от круга до любого полигона); спрайты можно отрезать друг от друга с использованием масок, заполнять градиентами и проверять на коллизии методом hitTest;

  • Движок ориентируется на таймлайн, а не на то что должно отображаться в текущем кадре;
  • Всё разнообразные события, связанные с контроллерами обрабатываются через механизмы Closure;
  • Анимации - переместить, масштабировать, вращать, пропасть - могут применяться и к одному объекту и к нескольким сразу и могут объединяться в цепочки (последовательные, одновременные, циклические);
  • Поддерживается DOM- и Canvas-рендеринг. WebGL-реднеринг планируется;
  • Если анимация применяется к DOM-эелемнту, она транслируется в CSS3-свойство;
  • Скрипты на выходе можно оптимизировать;
  • Есть класс Audio для проигрывания звука;

Строим сцену

Оставим из переданной нам от разработчиков функции pingpong.start только несколько строк:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// entrypoint
pingpong.start = function(){

    var director = new lime.Director(document.body),
        scene = new lime.Scene();

    director.makeMobileWebAppCapable();

    // set current scene active
    director.replaceScene(scene);

}

Не забудьте убрать ненужные строки goog.require. Я не буду напоминать про это в дальнейшем, как должен будет выглядеть заголовок файла вы всегда сможете посмотреть в конце статьи. Добавим в сцену три слоя - фон floor_, стены walls_ и доску, на которой будет происходить всё действие - board_:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var director = new lime.Director(document.body),
    scene = new lime.Scene(),

    floor_ = new lime.Layer().setPosition(0,0),
    walls_ = new lime.Layer().setPosition(0,0),
    board_ = new lime.Layer().setPosition(0,0);

scene.appendChild(floor_);
scene.appendChild(walls_);
scene.appendChild(board_);

. . .

Заготовка игрока

В отдельном файле player.js опишем класс игрока - это будет полигон в форме скейтборда (чтобы хорошо проверить как работают коллизии):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
goog.provide('pingpong.Player');

goog.require('lime.Polygon');

pingpong.Player = function() {
    goog.base(this);

    // ... собираем полигон
}
goog.inherits(pingpong.Player, lime.Polygon);

На месте комментария опишем точки полигона и зальём полупрозрачным синим. Так будет выглядеть игрок (в руководстве для координат полигона используются дробные числа от -1 до 1, но в текущей версии они у меня не заработали):

1
2
3
4
// -1,-2.5, 0,-3.5, 1,-2.5, 1,2.5, 0,3.5, -1,2.5, 0,1.5, 0,-1.5
this.addPoints(-50,-125, 0,-175, 50,-125, 50,125, 0,175, -50,125, 0,75, 0,-75)
    .setFill(0,0,210,.7)
    .setScale(.4);

Игрок

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

Пока что код равноценен вызову:

1
var playerOne = new lime.Polygon().addPoints(...).setFill(...);

Но позже мы добавим поведение к игроку и будет очевидно, что выделить класс было разумным. Давайте проверим, корректно ли отображается игрок в сцене - вернёмся к файлу pingpong.js… впрочем, что уж тянуть, давайте добавим сразу обоих игроков и отразим первого, чтобы они стояли лицом к лицу:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
. . .
goog.require('pingpong.Player');

. . .
    board_ = new lime.Layer().setPosition(0,0),

    playerOne = new pingpong.Player().setPosition(50,150).setRotation(180),
    playerTwo = new pingpong.Player().setPosition(400,150);

board_.appendChild(playerOne);
board_.appendChild(playerTwo);

. . .

Перед запуском в браузере, нужно произвести ещё одно мановение - обновить зависимости Closure (за счёт этого в .html могут быть включены только base.js и pingpong.js, а остальные внешние файлы подгружаются автоматически через goog.require). При этом в текущей версии библиотеки есть небольшой баг - при создании имя проекта не добавляется в файл ./bin/projects. Поэтому прежде нужно добавить строку pingpong в ./bin.projects, а потом обновить зависимости:

$ vim ./bin/projects   # add `pingpong` line
$ ./bin/lime.py update

Итак, вот что сейчас на экране:

Пляжники в синих плавках

Заготовка мячика

Создадим файл ball.js с таким содержимым:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
goog.provide('pingpong.Ball');

goog.require('lime.Circle');

pingpong.Ball = function() {
    goog.base(this);

    this.setFill(255,0,0,.7)
        .setSize(20,20);
}
goog.inherits(pingpong.Ball, lime.Circle);

Обновим зависимости:

$ ./bin/lime.py update

И добавим мячик на доску в pingpong.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
. . .
goog.require('pingpong.Ball');
. . .

    playerOne = new pingpong.Player().setPosition(50,150).setRotation(180),
    playerTwo = new pingpong.Player().setPosition(400,150),
    ball = new pingpong.Ball().setPosition(275,150);

board_.appendChild(playerOne);
board_.appendChild(playerTwo);
board_.appendChild(ball);

Пляжники в синих плавках с мячиком

Фон

Давайте зададим фон на поле с игроками, для каждого игрока половина поля своего цвета. Добавим к Director параметры размеров экрана игры:

1
var director = new lime.Director(document.body,600,480),

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

1
2
3
playerOne = new pingpong.Player().setPosition(40,240).setRotation(180),
playerTwo = new pingpong.Player().setPosition(600,240),
ball = new pingpong.Ball().setPosition(320,240);

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

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

1
2
3
4
5
6
7
8
9
floor_.appendChild(new lime.Sprite().setPosition(160,240)
                                    .setSize(320,480)
                                    .setFill(100,100,100));
floor_.appendChild(new lime.Sprite().setPosition(480,240)
                                    .setSize(320,480)
                                    .setFill(200,200,200));

board_.appendChild(...);
. . .

Пляжники в синих плавках с мячиком на асфальте

Заготовка стен

У стен будет совсем немного логики, но тем не менее тоже выделим их в отдельный класс. Стены будут размером 20x20. Создадим файл wall.js с таким содержимым:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
goog.provide('pingpong.Wall');

goog.require('lime.Sprite');

pingpong.Wall = function() {
    goog.base(this);

    this.setFill(255,255,0)
        .setSize(20,20);
}
goog.inherits(pingpong.Wall, lime.Sprite);

Обновим зависимости:

$ ./bin/lime.py update

И расставим стены вдоль краёв полотна в pingpong.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
. . .
goog.require('pingpong.Wall');
. . .

floor_.appendChild(...);

// horizontal walls
for (x = 10; x <= 630; x += 20) {
    walls_.appendChild(new pingpong.Wall().setPosition(x, 10));
    walls_.appendChild(new pingpong.Wall().setPosition(x, 470));
}
// vertical walls
for (y = 30; y <= 450; y += 20) {
    walls_.appendChild(new pingpong.Wall().setPosition(10, y));
    walls_.appendChild(new pingpong.Wall().setPosition(630, y));
}

board_.appendChild(...);

Всё, поле наконец готово - можно приступать к логике!

Пляжники в синих плавках с мячиком на серых квадратах, окружённые жёлтыми ящиками

Логика игроков

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
. . .

director.makeMobileWebAppCapable();

goog.events.listen(floor_,['mousedown','touchstart'],function(e){
    var player_ = (e.position.x <= 320) ? playerOne : playerTwo;
    player_.runAction(
            new lime.animation.MoveTo(
                        player_.alignBounds(player_.getPosition().x,
                                            e.position.y))
                              .setDuration(1));
});

director.replaceScene(scene);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
pingpong.Player.prototype.setMovementBounds = function(top,right,bottom,left) {
    this._moveBounds = new goog.math.Box(top,right,bottom,left);
    return this;
}

pingpong.Player.prototype.alignBounds = function(x, y) {
    if (this._moveBounds === undefined) return new goog.math.Coordinate(x, y);
    var size_ = new goog.math.Size(this.getSize().width * this.getScale().x,
                                   this.getSize().height * this.getScale().y);
    var newX = x, newY = y;
    if (x < (this._moveBounds.left + (size_.width / 2)))
                  newX = this._moveBounds.left + (size_.width / 2);
    if (x > (this._moveBounds.right - (size_.width / 2)))
                  newX = this._moveBounds.right - (size_.width / 2);
    if (y < (this._moveBounds.top + (size_.height / 2)))
                  newY = this._moveBounds.top + (size_.height / 2);
    if (y > (this._moveBounds.bottom - (size_.height / 2)))
                  newY = this._moveBounds.bottom - (size_.height / 2);
    return new goog.math.Coordinate(newX, newY);
}

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

Теперь в pingpong.js обновим определение игроков:

1
2
3
4
5
playerOne = new pingpong.Player().setPosition(40,240)
                                 .setRotation(180)
                                 .setMovementBounds(20,620,460,20),
playerTwo = new pingpong.Player().setPosition(600,240)
                                 .setMovementBounds(20,620,460,20),

И исправим событие, их перемещающее:

1
2
3
4
5
6
7
8
goog.events.listen(floor_,['mousedown','touchstart'],function(e){
    var player_ = (e.position.x <= 320) ? playerOne : playerTwo;
    player_.runAction(
            new lime.animation.MoveTo(
                    player_.alignBounds(player_.getPosition().x,
                                        e.screenPosition.y))
                              .setDuration(2));
});

Логика мяча

Для мяча понадобится несколько дополнительных функций. Одна позволяет ограничивать движение прямоугольным регионом, так же как и у и игрока, другая устанавливает скорость движения мяча, третья сбрасывает его положение в начальную точку (ball.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pingpong.Ball = function() {
    goog.base(this);

    this.setFill(255,0,0,.7)
        .setSize(20,20);

    this._xCoef = 1;
    this._yCoef = 1;

    this._resetPos = new goog.math.Coordinate(0, 0);
    this._velocity = 2;
}
goog.inherits(pingpong.Ball,lime.Circle);

pingpong.Ball.prototype.setMovementBounds = function(top,right,bottom,left) {
    this._moveBounds = new goog.math.Box(top,right,bottom,left);
    return this;
}

pingpong.Ball.prototype.setVelocity = function(velocity) {
    if (velocity) this._velocity = velocity;
    return this;
}

pingpong.Ball.prototype.setResetPosition = function(x, y) {
    this._resetPos = new goog.math.Coordinate(x, y);
    return this;
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pingpong.Ball.prototype.updateAndCheckHit = function(dt,playerOne,playerTwo) {
    var newPos_ = this.getPosition();
    var size_ = new goog.math.Size(this.getSize().width * this.getScale().x,
                                   this.getSize().height * this.getScale().y);
    newPos_.x += this._xCoef * this._velocity * dt;
    newPos_.y += this._yCoef * this._velocity * dt;
    var hitVBounds_ = false; // vertical bounds were hit
    if (this._moveBounds !== undefined) {
        if (newPos_.x <= (this._moveBounds.left + (size_.width / 2)))
                         { this._xCoef = 1; hitVBounds_ = true; }
        if (newPos_.x >= (this._moveBounds.right - (size_.width / 2)))
                         { this._xCoef = -1; hitVBounds_ = true; }
        if (newPos_.y <= (this._moveBounds.top + (size_.height / 2)))
                         this._yCoef = 1;
        if (newPos_.y >= (this._moveBounds.bottom - (size_.height / 2)))
                         this._yCoef = -1;
    }
    var p1catched_ = playerOne.catched(this.getParent().localToScreen(newPos_));
    var p2catched_ = playerTwo.catched(this.getParent().localToScreen(newPos_));
    if (hitVBounds_ && !p1catched_ && !p2catched_) {
        this.setPosition(this._resetPos.x,this._resetPos.y);
        return newPos_;
    } else if (p1catched_) { this.xCoef = 1; return null; }
      else if (p2catched_) { this.xCoef = -1; return null; }
    this.setPosition(newPos_.x, newPos_.y);
    return null;
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pingpong.Player.prototype.catched = function(pos) {
    var p = this.getPoints(),
        s = this.getScale(),
        r = this.getRotation(),
        plen = p.length,
        coord = this.screenToLocal(pos),
        inPoly = false;

    var rsin = Math.sin(r * Math.PI / 180),
        rcos = Math.cos(r * Math.PI / 180),
        csx = coord.x * s.x,
        csy = coord.y * s.y,
        crx = (csx * rcos) - (csy * rsin),
        cry = (csx * rsin) + (csy * rcos);
        crx = coord.x, cry = coord.y;

    if (plen > 2) {
        var i, j, c = 0;

        for (i = 0, j = plen - 1; i < plen; j = i++) {
            var pix_ = p[i].x, piy_ = p[i].y,
                pjx_ = p[j].x, pjy_ = p[j].y;

            if (((piy_ > cry) != (pjy_ > cry)) &&
                (crx < (pjx_ - pix_) * (cry - piy_) /
                    (pjy_ - piy_) + pix_)) {
                    inPoly = !inPoly;
                }
        }
    }

    return inPoly;
}

Установим все необходимые настройки при инициализации мяча в pingpong.js:

1
2
3
4
ball = new pingpong.Ball().setPosition(320,240)
                          .setMovementBounds(20,620,460,20)
                          .setVelocity(.2)
                          .setResetPosition(320,240);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
goog.events.listen(. . .);

var hitPos_;
lime.scheduleManager.schedule(function(dt){
    if (hitPos_ = ball.updateAndCheckHit(dt, playerOne, playerTwo)) {
       console.log('player',(hitPos_.x <= 320) ? 1 : 2,'is a loser');
    };
},ball);

director.replaceScene(scene);

Сообщение о проигрыше

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

1
2
3
4
5
6
7
ball = . . .
       .setResetPosition(320,240),

label = new lime.Label().setPosition(280,30)
                        .setText('').setFontFamily('Verdana')
                        .setFontColor('#c00').setFontSize(18)
                        .setFontWeight('bold').setSize(150,30);

Не забудем добавить лейбл на слой с доской:

1
2
board_.appendChild(ball);
board_.appendChild(label);

И, исправим вывод текста о проигрыше на лейбл вместо консоли:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
goog.events.listen(. . .);

var hitPos_ = null, defDelay_ = 500, delay_ = defDelay_;
lime.scheduleManager.schedule(function(dt){
    delay_ -= dt;
    if (delay_ <= 0) label.setText('');
    if (hitPos_ = ball.updateAndCheckHit(dt, playerOne, playerTwo)) {
       label.setText('player ' + ((hitPos_.x <= 320) ? 1 : 2) + ' is a loser');
       delay_ = defDelay_;
    };
},ball);

director.replaceScene(scene);

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

Марафет

Отлично, теперь давайте наведём небольшой марафет, чтобы продемонстрировать работу с градиентами и текстурами.

Сделаем фон приятного зелёно-травяного цвета - поменяем инициализацию фоновых спрайтов в pingpong.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
floor_.appendChild(new lime.Sprite().setPosition(160,240)
                                    .setSize(321,480)
                                    .setFill(new lime.fill.LinearGradient()
                                                     .setDirection(0,1,1,0)
                                                     .addColorStop(0,0,92,0,1)
                                                     .addColorStop(1,134,200,105,1)));
floor_.appendChild(new lime.Sprite().setPosition(480,240)
                                    .setSize(320,480)
                                    .setFill(new lime.fill.LinearGradient()
                                                     .setDirection(1,1,0,0)
                                                     .addColorStop(0,0,92,0,1)
                                                     .addColorStop(1,134,200,105,1)));

Сделаем игрокам (player.js) немного прозрачный синий морской градиент:

1
2
3
4
5
6
this.addPoints(-50,-125, 0,-175, 50,-125, 50,125, 0,175, -50,125, 0,75, 0,-75)
    .setFill(new lime.fill.LinearGradient()
                          .setDirection(0,1,1,0)
                          .addColorStop(0,0,0,210,.7)
                          .addColorStop(1,0,0,105,.7))
    .setScale(.4);

Мячу (ball.js) поставим текстуру с мячиком:

1
2
this.setFill('./ball.png')
    .setSize(20,20);

Стену (wall.js) раскрасим в бетонно-синий цвет и отнаследуем от RoundedRect:

1
2
3
4
5
6
7
8
pingpong.Wall = function() {
    goog.base(this);

    this.setFill(109,122,181)
        .setSize(20,20)
        .setRadius(3);
}
goog.inherits(pingpong.Wall, lime.RoundedRect);

Вот, теперь у нас всё выглядит много симпатичнее:

Мужчины в синих шортах на футбольном поле с детским мячиком

Компиляция

Итак, демонстрационная игра готова. Исходники, которые получились у меня:

pingpong.js | player.js | ball.js | wall.js | ball.png | pingpong.html

Теперь перепроверьте все goog.require - уберите неиспользуемые вызовы, затем обновите зависимости и соберите всё в один скрипт:

$ ./bin/lime.py update
$ ./bin/lime.py build pingpong -o pingpong/compiled/pp.js

Теперь в папку compiled можно скопировать pingpong.html и в заголовке поменять вызовы JavaScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE HTML>

<html>
<head>
    <title>pingpong</title>
    <script type="text/javascript" src="pp.js"></script>
</head>

<body onload="pingpong.start()"></body>

</html>

Резюме

Сначала я относился к движку немного скептически, представленные на сайте две (всего) игры чересчур каузальны, я не очень это люблю. Мало примеров и подробностей в документации и многовато всего нужно для установки. И ещё очень кислотный незамысловатый квадратик в favicon… :)

Но потом я поиграл в игру с числами и она оказалась довольно-таки захватывающей (похожа на Super 7 HD для iPad - попроще конечно, раз демка). А потом, когда потренировался при написании игры из статьи, всё оказалось довольно удобно, продумано и даже минималистично. Есть мелкие сырости и неосвещённые в документации вещи, но если код forward-compatible, то почему-бы и нет - ребята прямо сейчас исправляют все эти вещи.

Главное - это действительно не state-machine, которые сейчас модно делать - здесь можно отталкиваться от сценария игры, привязываясь к событиям, а не ко времени или текущему кадру, вам не надо думать как оптимизировать отрисовку многих объектов в следующем кадре - да, почти что Flash, жаль что без редактора.

Видео

LimeJS Engine demonstation on iPhone - PingPong game from Ulric Wilfred on Vimeo.

LimeJS Engine demonstation on Android - PingPong game from Ulric Wilfred on Vimeo.

LimeJS Engine demonstation on iPad - PingPong game from Ulric Wilfred on Vimeo.

(Видео записаны с помощью авторов движка)

Поиграть

Здесь можно попробовать поиграть (может глючить, потому что это очень упрощённая версия, сравнивайте пожалуйста ожидания работы на вашей платформе с приведёнными выше видео)

QRCode

P.S. Отдельное спасибо lazio_od, он помогал мне в тестировании одновременно с авторами движка.

Наверх