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

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

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>