Разработка на коленке

"тут должна быть красивая цитата о программировании"

Концепция HTML5 Sparkling Tail и первые шаги

2015-01-06 23:40
html5 Sparkling Tail overview

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

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

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

Старт разработки игры про Sparkling Tail

2014-07-29 23:10
Новый Sparkling tail

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

Главный персонаж - реинкарнировавшийся Sparkling Tail, только теперь он научился ходить, и ему больше не нужно прыгать словно гусеница, как раньше. Он будет жить в своём небольшом, но агрессивном мире. Почти всё, что есть в этом мире (надо бы этому миру название придумать), крайне вредно для Sparkling Tail'a, поэтому ему придётся избегать встречи с этими опасными штуками.

Разработка игры будет с использованием Phaser. Работать должна в десктопных браузерах и на андроидовском Chrome (это единственное мобильное железо, которое у меня есть, тестировать на IPhone и WinPhone не могу).

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

Totem Destroyer на javascript + Phaser

2014-07-21 23:10
Картинки игровых объектов

У меня сложилось впечатление, что TotemDestroyer - это своеобразный helloworld для тех, кто начинает разрабатывать игры, как когда-то гостевая книга была первым проектом для веб-программистов (сейчас пишут приложение для микроблогинга). Пусть у меня тоже будет свой totem destroyer.

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

Spritesheet

Тут всё просто. После того, как я нарисовал в Inkscape все необходимые для меня картинки, то сгруппировал их вместе, выровнял центры, поставил промежуток в 1 пиксель и сохранил. В начале поста эти же спрайты, только с промежутком в 10 пикселей.

Загрузка картинок:

game.load.spritesheet('spritesheet', 'sprites.png', 64, 64, 7, 0, 1);

После этого можно использовать картинки по номеру фрейма.

Кнопка

Сделать кнопку несложно:

game.add.button(WIDTH - 84 + OFFSET, OFFSET + 20, 'spritesheet',
                recreateLevel, this, 1, 0, 2, 0);

Четвёртым параметром идёт обработчик клика на кнопку, у меня там перезапуск уровня (ну в данном случае всей игры).

World

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

game.world.setBounds(OFFSET, OFFSET, WIDTH + OFFSET * 2, HEIGHT + OFFSET * 2);

Грабли

Не обошлось и без граблей. Скорее всего это есть в документации, но я начал использовать Phaser недавно, поэтому нахожу много сюрпризов. Одним из них стало то, что позиция простого спрайта считается от верхнего левого угла, а у физического объекта (для которого вызвали game.physics.p2.enable(block)) - от центра. Из-за этого не сразу понял, почему у меня прыгают блоки по всему полю.

Игра

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

Ну и сама игра:

Исходник (totemdestroyer.js):

(function(){
    var OFFSET = 128;

    var WIDTH = $(window).width();

    var HEIGHT = $(window).height();

    var removables = [];

    var statics = [];

    var totem = {
        sprite: null,

        freeze: function() {
            if (this.sprite) {
                this.sprite.body.velocity.x = 0;
                this.sprite.body.velocity.y = 0;
            }
        },

        isFall: function() {
            if (this.sprite) {
                if (this.sprite.body.y > (HEIGHT + OFFSET)) {
                    return true;
                }
            }
            return false;
        },

        isMoving: function() {
            if (this.sprite) {
                var nearlyToFreeze = 0.025;
                return Math.abs(this.sprite.body.velocity.x) > nearlyToFreeze ||
                       Math.abs(this.sprite.body.velocity.y) > nearlyToFreeze;
            }
        },

        create: function(x, y) {
            if (this.sprite) {
                this.sprite.destroy();
            }

            this.sprite = createBlock(x, y, 6, false);
            this.sprite.body.setCircle(32);
        },

        destroy: function() {
            if (this.sprite) {
                this.sprite.destroy();
            }
            this.sprite = null;
        }
    };


    var createBlock = function(x, y, frame, isStatic, isRemovable) {
        var block = game.add.sprite(x, y, 'spritesheet', frame);
        game.physics.p2.enable(block);

        block.body.static = isStatic;
        block.removable = isRemovable;

        if (block.removable) {
            removables.push(block);
        }
        else {
            statics.push(block);
        }

        return block;
    };


    var hasRemovable = function() {
        return removables.length > 0;
    };


    var win = function() {
        game.stage.backgroundColor = '#00CD00';
    };


    var lose = function() {
        game.stage.backgroundColor = '#990000';
    };

    var recreateLevel = function() {
        game.stage.backgroundColor = '#DECD87';

        var block;
        while(block = removables.pop()) {
            block.destroy();
        }
        while(block = statics.pop()) {
            block.destroy();
        }
        totem.destroy();

        var worldCenterX = game.world.centerX;

        var groundPositionY = game.world.height - 256;
        createBlock(worldCenterX, groundPositionY, 3, true, false);
        for (var i = 0; i < 4; i++) {
            createBlock(worldCenterX - 64 * (i + 1), groundPositionY, 3, true, false);
            createBlock(worldCenterX + 64 * (i + 1), groundPositionY, 3, true, false);
        }

        var onGroundY = groundPositionY - 64;

        createBlock(worldCenterX, onGroundY - 1, 5, false, true);
        createBlock(worldCenterX - 80, onGroundY - 1, 5, false, true);
        createBlock(worldCenterX + 80, onGroundY - 1, 5, false, true);
        createBlock(worldCenterX + 40, onGroundY - 64 - 2, 5, false, true);
        createBlock(worldCenterX - 40, onGroundY - 64 - 2, 5, false, true);
        createBlock(worldCenterX, onGroundY - 129 - 3, 4, false, false);

        totem.create(worldCenterX, onGroundY - 197);

        game.input.onDown.add(onClick, this);
    };


    var onClick = function(pointer) {
        var worlPointerPosition = {'x': pointer.position.x + OFFSET, 'y': pointer.position.y + OFFSET};
        var bodiesClicked = game.physics.p2.hitTest(worlPointerPosition);

        for (var i = 0; i < bodiesClicked.length; i++) {
            var sprite = bodiesClicked[i].parent.sprite;
            if (sprite.removable) {
                var i = removables.indexOf(sprite);
                if (!(i < 0)) {
                    removables.splice(i, 1);
                }
                sprite.destroy();
            }
        }

    };


    var onUpdate = function() {
        if (! hasRemovable()) {
            if(! totem.isMoving()) {
                totem.freeze();
                win();
            }
        }

        if (totem.isFall()) {
            lose();
        }
    };


    var onCreate = function() {
        game.world.setBounds(OFFSET, OFFSET, WIDTH + OFFSET * 2, HEIGHT + OFFSET * 2);
        game.physics.startSystem(Phaser.Physics.P2JS);
        game.physics.p2.gravity.y = 1000;
        game.physics.p2.friction = 3.0;
        game.physics.p2.restitution = 0;

        game.add.button(WIDTH - 84 + OFFSET, OFFSET + 20, 'spritesheet',
                        recreateLevel, this, 1, 0, 2, 0);

        recreateLevel();
    };


    var onPreload = function() {
        game.load.spritesheet('spritesheet', 'sprites.png', 64, 64, 7, 0, 1);
    };


    var game = new Phaser.Game(
        WIDTH, HEIGHT, Phaser.CANVAS, '',
        {preload: onPreload, create: onCreate, update: onUpdate}
    );
})();

index.html:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <title>Totem destroyer with pahser</title>
    <style type="text/css" media="all">
    * {
        margin: 0;
        padding: 0;
    }
    </style>
</head>
<body>
    <script src="jquery.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="phaser.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="totemdestroyer.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

Phaser: летающие шарики, которые никогда не останавливаются

2014-06-29 19:40
Луна

Вот такая луна получилась у меня после 20 минут рисования в Inkscape. Конечно невесть что, но для начала сойдёт. Эта луна будет у меня летать в компании таких же.

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

Итак, чего я хотел:

  • игровой мир, в котором летают шарики;
  • движение никогда не должно останавливаться;
  • по клику - ставить на паузу.

Вот и все нехитрые требования.

Сделать шарики несложно, достаточно просто создать объект и установить тип для body:

moon.body.setCircle(radius);

Самым сложным оказалось организовать вечное движение. Дело в том, что при столкновении скорость движения затухает, а мне этого хотелось избежать. Поначалу я искал возможность выставить минимальную скорость у объекта или какой-нибудь постоянный импульс, но не нашёл. Потом хотел в update корректировать скорость, т.е. вычислять направление, а потом вычислять новый вектор скорости. Но с этим тоже не задалось, картинка всё время дёргалась. В конце концов нужного мне поведения добился, установив упругость в 1, т.е. при столкновениях с грацицами и между лунами, не происходит потеря энергии, это меня устроило:

var moonMaterial = game.physics.p2.createMaterial('moonMaterial');
var worldMaterial = game.physics.p2.createMaterial('worldMaterial');

var moon2worldCcontactMaterial = game.physics.p2.createContactMaterial(
    moonMaterial, worldMaterial, { restitution: 1.0 });
var moon2moonContactMaterial = game.physics.p2.createContactMaterial(
    moonMaterial, moonMaterial, { restitution: 1.0 });

game.physics.p2.setWorldMaterial(worldMaterial);

Ну а выставлять на паузу по клику и вовсе просто:

game.paused = !game.paused;

Вот что получилось в итоге:

Исходник bouncingmoons.js:

(function() {

    var onPreload = function() {
        game.load.image('moon', 'moon.png');
    };

    var onCreate = function() {
        game.stage.backgroundColor = '#373e48';
        game.physics.startSystem(Phaser.Physics.P2JS);
        game.physics.p2.gravity.y = 0;
        game.physics.p2.friction = 0;
        game.physics.p2.applyDamping = false;

        var moonMaterial = game.physics.p2.createMaterial('moonMaterial');
        var worldMaterial = game.physics.p2.createMaterial('worldMaterial');

        var moon2worldCcontactMaterial = game.physics.p2.createContactMaterial(
            moonMaterial, worldMaterial, { restitution: 1.0 });
        var moon2moonContactMaterial = game.physics.p2.createContactMaterial(
            moonMaterial, moonMaterial, { restitution: 1.0 });

        game.physics.p2.setWorldMaterial(worldMaterial);

        var maxMoonsNumber = 5;
        var maxVelocity = 400;

        var moons = game.add.group();
        for(var i = 0; i < maxMoonsNumber; i++) {
            var moon = moons.create(50 + 110 * i, 100, 'moon');
            game.physics.p2.enable(moon);

            var scale = 0.3 + 0.05 * i;
            moon.scale = {x: scale, y: scale};
            moon.body.setCircle(60 * scale);

            moon.damping = 0;
            moon.angularDamping = 0;

            moon.body.velocity.x = Math.floor(Math.random() * maxVelocity + 1);
            moon.body.velocity.y = Math.floor(Math.random() * maxVelocity + 1);

            moon.body.setMaterial(moonMaterial);
        }

        game.input.onDown.add(onClick, this);
    };

    var onClick = function() {
        game.paused = !game.paused;
    };

    var WIDTH = $(window).width();
    var HEIGHT = $(window).height();
    var game = new Phaser.Game(
        WIDTH, HEIGHT, Phaser.CANVAS, '', {preload: onPreload, create: onCreate});
})();

index.html:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <title>moons</title>
    <style type="text/css" media="all">
    * {
        margin: 0;
        padding: 0;
    }
    </style>
</head>
<body>
    <script src="jquery.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="phaser.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="bouncingmoons.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

Разработка игр на javascript + html5, пробую Phaser

2014-06-25 22:10

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

Пробежался по списку движков, выбрал комбайн пообъёмистее - phaser. В целом я рассуждал так: сперва разберусь, как это делается вообще, а там можно будет переходить к микрофреймворкам или писать самому на голом javascript, рисуя на canvas.

В качестве физического движка выставил p2js, вроде он умеет работать с окружностями, а arcade - нет, разберусь со временем.

Сделал игровой мир с гравитацией, направленной вверх. По клику появляются камменые блоки, которые под действием гравитации летят вверх. Количество блоков ограниченно, когда их слишком много - старые удаляются. Что интересно - на мобильном тоже работает, проверял в хроме на андроиде.

Ну и исходник

(function() {
    var boxes = [];
    var maxBoxesNumber = 8;

    var preload = function () {
        game.load.image('box', 'box.png');
    };

    var create = function() {
        game.stage.backgroundColor = '#554400';
        game.physics.startSystem(Phaser.Physics.P2JS);
        game.physics.p2.gravity.y = -250;
        game.input.onDown.add(click, this);
    };

    var click = function(pointer) {
        if (boxes.length > maxBoxesNumber) {
            var boxToRemove = boxes.shift();
            boxToRemove.kill();
        }
        var box = game.add.sprite(pointer.position.x, pointer.position.y, 'box');
        game.physics.p2.enable(box);
        boxes.push(box);
    };

    var game = new Phaser.Game(
        $(window).width(), $(window).height(), Phaser.CANVAS, '',
        {preload: preload, create: create});

})();
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <title>HelloBoxes</title>
    <style type="text/css" media="all">
    * {
        margin: 0;
        padding: 0;
    }
    </style>
</head>
<body>
    <script src="jquery.js" type="text/javascript" charset="utf-8"></script>
    <script src="phaser.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="helloboxes.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>