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

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

Match3Cat - игрушка про котиков на Reactjs

2017-10-17 23:35
cat

На работе у меня стало очень много ReactJS, и мне стало интересно, как будет выглядеть разработка игры на этом фреймворке.

История

Это просто результат любопытства из категории "а что будет, если...". В итоге получилась вот такая штука.

Сперва в игре список котов был сделан одномерным массивом, который было очень удобно обновлять в Redux (где каждый раз нужно создавать новую копию данных, чтобы внести изменения). А разбиение на таблицу добился простой эмуляцией двумерного массива на одномерном списке (операция деления по модулю очень помогает). Не то чтобы это было нужно, просто было интересно писать алгоритм. И было всё довольно неплохо до того момента, пока мне не понадобилась анимация в момент совпадения котиков. Тут мне нужно было знать, где находился котик в момент совпадения по цветам, а значит нужно было хранить позицию. Чтобы не возиться с дополнительным хранилищем, котики были переделаны в словарь, а вместо одномерного списка - двумерный массив. Ну а ещё каждому котику присваивался уникальный номер, чтобы генерировать уникальный же key

const createCat = (() => {
    let nextCatNumber = 0;

    return ((x, y) => {
        return {
            num: nextCatNumber++,
            kind: Math.floor(Math.random() * CATS_NUMBER),
            x: x,
            y: y,
        };
    })
})();

В целом, получилось довольно интересно, но писать игры на ReactJS мне не понравилось, не для этого фреймворк предназначался.

Теперь настал черёд воинственных прямоугольных попрыгунчиков .

Ссылка на игру

match3cat game

Ресурсы

Исходный код там - https://github.com/grigoriytretyakov/react-match3-game

Игра там - http://tinycode.ru/games/match3cat/

Lightbox - игрушка про квадратик (Phaser, ECMAScript6)

2017-03-26 17:20
beast

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

Основная цель - попробовать ECMAScript6 в работе, а ещё собрать в итоге игру в один файл.

История

Первоначальная идея была сделать кубик, состоящий из света, а жить он должен был в тёмном мире, где всё хочет его убить. Выживать он должен был подпитываясь энергией от пролетающих мимо "энергетических заправок". Но TN-матрица на моём ноутбуке скорректировала планы, потому что тёмная игра смотрелась ужасно, играть было неудобно. Поэтому фон стал светлым, а кубик тёмным. Тут бы впору переименовать его в "Dark energy box", но я решил оставить всё как есть.

Ссылка на игру

lightbox game

Ресурсы

Исходный код там - https://bitbucket.org/grigoriytretyakov/lightbox-game

Звуковые эффекты делал там http://www.bfxr.net/

Музыку для игры взял там http://opengameart.org/content/game-game

Игра там - http://tinycode.ru/games/lightboxgame/

Концепция 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>