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

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

Избавился от Marker With Label

2014-09-04 19:50

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

А spritesheet сгенерировал скриптом на питоне, который выглядит примерно так:

import math

from os import path
from PIL import ImageFont, Image, ImageDraw


def here(*pathchunks):
    return path.abspath(path.join(path.dirname(__file__), *pathchunks))


def spritesheet(image, labels, columns):
    original_width, original_height = image.size

    font_name = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'
    font = ImageFont.truetype(font_name, 10)

    sheet_items = []
    for label in labels:
        item_image = image.copy()

        item_draw = ImageDraw.Draw(item_image)

        _, text_height = item_draw.textsize(label)
        text_width, _ = font.getsize(label)

        item_draw.text(
            ((original_width - text_width) / 2, (original_height - text_height) / 2),
            label, (255, 255, 255), font=font)

        sheet_items.append(item_image)

    sheet_width = original_width * columns
    lines = int(math.ceil(len(labels) / float(columns)))
    sheet_height = original_height * lines
    sheet = Image.new(mode='RGBA', size=(sheet_width, sheet_height), color=(0, 0, 0, 0))

    for number, item_image in enumerate(sheet_items):
        x = number % columns
        y = int(number / columns)
        sheet.paste(item_image, (x * original_width, y * original_height))

    sheet.save(here('sheet.png'), 'PNG')


if __name__ == '__main__':
    spritesheet(
        Image.open(here('image.png')), [str(i) for i in range(25)], 10
    )

Полезные вещи из мира JavaScript

2014-08-15 17:50

Основной мой профиль - разработка на Python серверной части web-приложений. Довольно часто пишу что-то консольное на том же питоне, что крутится на серверах во мраке линуксовых терминалов. Во всяком случае так было раньше. В последнее время стал писать на JavaScript гораздо больше, вот игрушки пробую делать, а совсем недавно делал на работе приложение с картами, картинками и прочей красотой - wifi.osmino.com. В качестве бонуса - опыт работы с новыми библиотеками, в качестве минуса - очень много сил ушло на разработку этого проекта, после работы не было желания что-либо делать, а особенно на JavaScript, поэтому Sparkling Tail остался в том же состоянии, что и был.

Marker With Label

Эта штука позволяет делать надписи на маркерах для google maps, очень пригодилась, потому что нужно было обозначать на скоплениях сетей примерное количество wifi-сетей в группе. Сайт проекта.

waiteForImages

Когда DOM-дерево формируется динамически javascript'ом, а среди вставленных элементов есть картинки, то лучше не показывать их пока они не загрузились. Эта библиотека помогает это сделать, без необходимости самому создавать эти обработчики на каждый img. Сайт проекта.

SimpleBox

А вот с этой штукой пришлось иметь дело по воле случая. Верстальщик использовал эту штуку для отображения подробностей о продукте, а я прикрутил её для показа галереи картинок в списке отзывов. Сайт проекта.

UglifyJs

Эту штуку использовал для сжатия исходников (и буду использовать для сжатия исходников своих игр). Однако для того, чтобы применить её пришлось узнать немного про nodejs.

Для начала нужно было установить последнюю версию nodejs, нашёл в сети кусок скрипта, который растащили по сети все кому не лень, утащу и я:

sudo apt-get update
sudo apt-get install -y python-software-properties python g++ make
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs

Потом установил UglifyJs, и запустил сжатие:

uglifyjs -o dest_filename src_filename

Старт разработки игры про 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>