Пользователь
0,0
рейтинг
3 декабря 2012 в 23:14

Разработка → Реверс-инжиниринг арканоида DX-ball, или Новая жизнь старой игры

Поздравить всех хабравчан с неофициальным днем компьютерной графики! В этот день я хочу рассказать вам о том как я сделал онлайн версию старой игры.

Возможно многим знакома игра DX-Ball, Я играл в нее еще в дошкольном возрасте, а уже в школе коротал за ней уроки информатики. Поэтому мне было интересно делать ее на HTML5.



Немного о работе

Я не буду приводить здоровые куски кода, и объяснять как что работает, я расскажу о там, как я разбирал эту игру, а о самой игре скажу лишь, то, что она сделана только на canvas'е. Я хотел сделать ее максимально похожей на оригинальную, и оставить как можно больше файлов без изменения, единственное что изменилось, так это список рекорд — он стал «бесконечным».
В начале задумывалось перерисовывать только те элементы котором изменяются, но из за большого количества багов, которые возникали в связи с этим, я остановился на варианте полной перерисовки кадра. И несмотря на то что игра может растягивается на весь экран, ее разрешение всегда остается 640x480px, впрочем как и в оригинале.

Разберем состав игры:

фалы *.pcx — это растровые фоны игры такие как начальная заставка с описанием бонусов, и финальный «High Score». я не стал их разбирать, но об устройстве PCX можно почитать в интересной серии статей, тут же на хабре.
*.mds — MIDI файлы музыки, сконвертировать их в *.mp3 мне помог savex.
В файле Default.bds хранится информация о расположении кирпичей на всех 50 уровнях
На одном уровне 202 кирпичей, которые записываются побайтно в этот файл.
От этого и получается 202×50 = 20 КБ
*.sbk — файлы растровых шрифтов, и остальной графики (кирпичи, ракетки и др.)
Информации о том как устроен этот файл я негде не нашел, поэтому пришлось разбираться самому, и вот что я выяснил:
Файлы этого типа содержат в себе что то вроде этого:

Графическое отображение Sysfont.sbk
изображение кликабельно.
Структура этого файла немного сложнее чем Default.bds, в нем по очереди описываются символы,
Первые 4 байта файла — это заголовок файла, он содержат в себе информацию о том сколько символов (изображений) содержит в себе файл.

После заголовка идет последовательное описание символов (изображений), как показано на скриншоте файл Sysfont.sbk содержит 94 символа. Ширина и высота первого символа указанны в 4'ом и 8'ом байтах. 12'ый байт — это номер символа из таблицы ASCII, 0x41 = A, если он равен 0x00, то это не символ, а какая то картинка. Далее идут еще несколько байтов, в нашем случае 130 (13×10), начиная с 17'ого байта — это и есть растр символа (на рисунке обозначено зеленым). Затем все это повторяется еще 93 раза (начиная с байта означающего ширину символа).

Изображения (символы) отрисовываются снизу вверх, слева направо 1 байт = 1 пиксель, значение байта — это номер цвета.
Как я понял, в игре используется 2 цветовых режима, первый используется в самой игре, а второй только в заставке.
вот 2 схемы, каждый цвет имеет 2 номера, верхний — это номер цвета в Dec, а нижний в Hex:


Вот пример первых 3-х символов файла Sysfont.sbk с использованием первой схемы:


Ах да, ноль в обоих таблицах это прозрачный цвет, а цвета в первой схеме начиная с 224 по 231 — динамические, то есть смещаются на один цвет каждый кадр, что бы образовать вот такую анимацию image

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



Для решения этой проблемы существует специальный байт, идущие после обозначения символа, (на первом скриншоте он подчеркнут синем).
У символов gj этот байт равен 0xFD = 253. а у ^' этот байт равен 0x08 = 8. Это как раз и есть смещение символа относительно остальным, но почему же вместо отрицательного смещения такие большие числа? Дело в том, что таков вид записи отрицательных чисел, в одном байте 128 отрицательных и 128 положительных чисел. Если наш байт меньше 128, то мы его не трогаем, а если больше, мы просто вычитаем из него 256.
И получаем превосходный результат:


Перед запуском игры все изображения (символы) отрисовываются в том же канвасе, что и игра, я сохраняю их как картинки,
то есть: char[...].img.src = canvas.toDataURL("image/png");
Так как putImageData() гараздо медленнее чем drawImage() и это не единственный минус.
putImageData, не накладывает изображение сверху, а заменяет его полностью.
image
В процессе разбора игры я обнаружил два секретных бонуса, которые не используются в оригинальной игре :-)

не используемые бонусы

Вся анимация в игре оптимизирована с помощью requestAnimationFrame, но разные устройства выдают разные показатели FPS. Для того чтобы мяч летал с одинаковой скоростью не в зависимости от fps я умножил скорость мяча на коэффициент delta который рассчитывался по следующей формуле: delta = 1000/fps/60;, мой ноутбук успевал отрисовывать около 55 кадров в секунду, но периодически зависал, и проглатывал более 30 кадров за раз, из за этого зависания коэффициент delta рассчитывался не верно и мяч приобретал не контролируемую скорость, чтобы от от этого избавится я решил усреднять fps за 4 секунды, поэтому он так редко обновляется.

В предыдущей игре Doodle Jump я дал пользователям возможность вставить ее iframe на любой сайт, в результате чего свыше 7000 пользователей ежедневно играли в эту игру на сторонних сайтах совершенно разной тематики, от личных блогов до звукозаписывающих студий.
Поэтому и в DX-ball я не отнимаю такой возможности, более того игра гибко настраивается для вставки на сайт.

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

Ссылка на игру: DX-Ball.ru

P.S. Игра еще сыровата, в ней возможны баги и зависания, так что буду благодарен, если вы напишите о недочетах в комментариях.
Бородин Максим @BorodinKO
карма
53,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (62)

  • +5
    Пропал вечер. Спасибо. :)
  • +5
    Помню, много времени провёл, ставя очередной рекорд в dx-ball! Сейчас с удовольствием освежил. Очень здорово, но впечатление портят лаги при стрельбе, и отсутствие кнопки Esc, например :)
    • 0
      Про Esc совсем забыл, завтра поправлю.
    • 0
      Только вчера скачал сабж чтобы поностальгировать, и вот сегодня утром открываю хабр…
  • +14
    Круто :) Не хватает только захвата курсора :)
  • +2
    почему-то иногда при старте уровня некоторые блоки самоликвидируются
    • +1
      Я так понимаю что это отголосок предыдущего раунда.
  • +3
    Полноэкранный режим не работает — из него вываливаешься от первого же движения мышью. Браузер FF.
  • 0
    Со второго раза.
  • +1
    Временами жутчайше тормозит, FPS около 1-2. Safari 6.0.2
  • +2
    Помню у меня винт сгорел, так я с одной дискеты дос запускал, а со второй эту игру. Было время!
    • 0
      DX-Ball не работал под DOS, он же DirectX использовал.
  • +1
    Ловлю Жесткие лаги и падение фпс на сафари
  • +1
    Chromuim 22, Linux — падение вкладки (Ow, Snap!) через полминуты игры или около.
    Firefox (Iceweasel 10) — фризы при высоком FPS (держится около 50-60).
    • 0
      На Chrome 25, Win7 — то же самое. Постоянно падает вкладка. По моему это случается в момент попадания шарика во «взрывающиеся блоки» в левой части экрана на самом первом уровне.
      • 0
        Может, у нас аппаратного ускорения нет? Или это баг Хрома?
  • 0
    Спасибо, круто.
    Стрелялка пробивает «желтые» блоки на 1 уровень.
  • +1
    Спасибо за ремейк, с удовольствием играл в оригинал.

    Шарик после ускорения движется слишком быстро. В оригинале такого не было.

    Был момент, когда выпало ускорение шарика, и пока оно падало я упустил шарик. Т.е. не поймал ни то ни другое (и даже не пытался поймать ускорение). После рестрта шарик двигался на двойной скорости.

    Очень любопытные косяки :)
    • 0
      Пока что скорость мяча крепко привязана к fps, а не переходах с уровня на уровень (при пропуске мяча) fps падает, при восстановлении игры fps не успевает нормализоваться, из за чего скорость увеличивается. Буду править.
  • +11
    Это великолепно! Видно, что работа проведена огромная, снимаю шляпу :)

    Но раз уж мы на хабре, то вот (вбивать в консоль):
    user.lives++; // добавляет одну жизнь
    user.score += 1000; // добавляет 1000 очков
    bonus.fireball = true; // огненный мяч
    bonus.magnet = true; // магнит на доску
    bonus.thru = true; // мяч проходит через все
    bonus.shooting = true; // пушки на доске
    
    function createlevel(a){cl=!1;shadow.aframe=20;clearTimeout(lightning.timout);audioFile.Voltage.pause();shadow.drawing=!0;for(y=remains=0;20>y;y++)for(x=0;20>x;x++)clearTimeout(bricks[x][y].timeout),bricks[x][y].type=file.charCodeAt(20*y+x+400*(a-1)),bricks[x][y].aframe=0,0!=bricks[x][y].type&&2!=bricks[x][y].type&&remains++,8==bricks[x][y].type&&(bricks[x][y].aframe=23)};
    user.level++; // текущий уровень
    createlevel(user.level); // кидает на следующий мгновенно
    
  • +1
    Вчера смотрел прохождение чип и дейла 2. Ностальгия такая. Вот бы кто переиздал в HD или с измененной графикой, был бы повод пройти.
  • +3
    В последней опере 9 fps :(
  • 0
    Перед запуском игры все изображения (символы) отрисовываются в том же канвасе, что и игра, я сохраняю их как картинки,
    то есть: char[...].img.src = canvas.toDataURL(«image/png»);
    Так как putImageData() гараздо медленнее чем drawImage() и это не единственный минус.
    putImageData, не накладывает изображение сверху, а заменяет его полностью.

    Это ошибка. Вы можете отрисовывать ваш canvas точно так же, как отрисовываете картинку:
    ctx.drawImage( canvas, fromX, fromY )
    

    Канвас и Картинка — почти одно и то же.

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

    Вот это зря. Стоило таки побороть баги ;)

    Вообще не вижу смысла прямо портироовать код. Можно сделать быстрее, короче и оптимизированнее, если писать код с нуля, пусть это будет и старая идея.

    Ну и, естественно, на LibCanvas — получим код поддерживаемый, расширяемый, на базе кучи готовых вещей, оптимизированный и, при этом, работающий плавно даже в Опере ;)

    Я писал арканоид для демки


    Написана на девелоперской версии библиотеки, она есть в публичном доступе. Можно заюзать существующий код, так что, при желании — пишите в скайп shock13666
    • 0
      Щас сам пишу арканоид, но блин застрял на collision detection, может кто посоветует как лучше обрабатывать столкновения с блоками и отражение мячика от них как у вас?
      • 0
        Сначала определяю те кирпичи, которые находятся вокруг мяча, затем проверяю их так:
        if(bricks[x][y].type!=0&&
        this.x>bricks[x][y].x-5&&
        this.x<bricks[x][y].x+35&&
        this.y>bricks[x][y].y-5&&
        this.y<bricks[x][y].y+20){
            действия с кирпичом
        }
        

        this — мяч
        bricks — кирпич
        Конечно это не самое лучшее решение.
        • 0
          Спасибо за подсказку, попробую.
          • 0
            Я по координатам в своей поделке сверял. А потом сверял пересекается ли шарик с квадратным кирпичём, как-то так.
            • 0
              У меня проблема в том что шарик может не с одним блоком сразу столкнуться…
      • 0
        Щас сам пишу арканоид, но блин застрял на collision detection, может кто посоветует как лучше обрабатывать столкновения с блоками и отражение мячика от них как у вас?

        Мне было важно, чтобы оно корректно работало даже при лагах (не пролетало сквозь объекты, словно тех нету). Но при этом чтобы корректно работало при маленьких расстояниях (залетело в линию между двумя блоками и ударами влево-вправо поднимается вверх). Для этого я бъю пройденное расстояние на отрезки длиной в 5 пикселей и анализирую их. За 5 пикселей шарик не пролетит никакой элемент. Потому я просто проверяю, находится ли следующая точка в пересечении с платформой или блоком с разными оптимизациями вроде не проверяния блоков, которые за пределами индекса (по сути каждый раз я проверяю, не входит ли шарик в один из ближайших 9 блоков)

        Файл ball.js
    • 0
      В начале задумывалось перерисовывать только те элементы котором изменяются, но из за большого количества багов, которые возникали в связи с этим, я остановился на варианте полной перерисовки кадра

      Вот это зря. Стоило таки побороть баги ;)

      Shock как всегда прав, с этой штукой в канве играться не стоит, простая игра на канве может загрузить проц. по полной перерисовывая весь экран целиком в каждом кадре.
      Shock, а не видел хорошего материал почитать на эту тему? Просто вот интересно, заводить у каждого объекта поле «сдвинулся» и если оно истинно перерисовывать объект, меняя после перерисовки поле на false? Как-то не изящно выходит ))
      • 0
        Shock, а не видел хорошего материал почитать на эту тему? Просто вот интересно, заводить у каждого объекта поле «сдвинулся» и если оно истинно перерисовывать объект, меняя после перерисовки поле на false? Как-то не изящно выходит ))

        И не только «сдвинулся», но и «увеличился», «перекрасился», «развернулся» и всё остальное.
        Ну у меня в последнем LibCanvas сделано так:
        Каждый кадр делится на два этапа — этап просчётов и этап отрисовки. В первом происходят математические изменения объектов — смещаются координаты, меняются цвета, свойства, удаляются лишние, запускаются анимации и т.д.
        Если объект хоть как-то меняется — вызываем у него «redraw». Теперь он стоит в очереди на отрисовку. Этот метод можно вызывать сотни раз — он просто ставит флаг, что необходимо перерисовать.
        Потом начинается этап рендеринга. В этот момент все объекты уже изменились и теперь надо отрисовать новую ситуацию. Стираем старое (если два объекта накладывались друг на друга, то надо перерисовывать их оба, даже если redraw был вызван только у одного из них). Отрисовываем новое.

        На практике это выглядит как-то так (псевдокод):

        animate({
          object: element,
          props: { fromX: 100 },
          onTick: element.redraw
        });
        

        Тут написано — «плавно измени свойство объекта fromX до 100 и каждый раз при изменении его перерисуй».

        Т.к. все анимации используют один и тот же таймер — это всё происходит синхронизированно.

        Ну или реальный пример:
        var helper, vector, target;
        
        // Глобализуем свойства LibCanvas
        LibCanvas.extract();
        
        // Создаём приложение размером 600*400
        helper = new App.Light(new Size(600, 400));
        
        // теперь внутри target у нас всегда актуальные координаты мыши относительно приложения
        target = helper.mouse.point;
        
        /* Создаём векторную фигуру - круг, радиусом 20 пикселей
         * с центром в точке 100/100
         * тёмно-красная середина и красная граница
         *
         * Этот круг у нас будет основным объектом
         */
        vector = helper
        	.createVector( new Circle(100, 100, 20), { zIndex: 2 })
        	.setStyle({ stroke: '#900', fill: '#300' });
        
        // При клике по холсту дописываем координату в очередь на движение
        helper.mouse.events.add('click', function () {
        	// создаём маленькую зелёную точку, которая будет обозначать цель нашего движения
        	var targetVector = helper
        		.createVector( new Circle(target.clone(), 2) )
        		.setStyle({ fill: '#0f0' });
        
        	// добавляем анимацию перемещения оригинального объекта в стек
        	vector.animate({
        		props: {
        			// плавно изменяем координаты центра
        			'shape.center.x': target.x,
        			'shape.center.y': target.y
        		},
        		fn: 'elastic-out',
        		// за одну секунду
        		time: 1000,
        		// при каждом смещении перерисовываем вектор
        		onTick: vector.redraw,
        		// при завершении движения удаляем точку-цель движения
        		onComplete: function () {
        			targetVector.destroy();
        		}
        	});
        });
        
    • 0
      А у вас молоко убежалоУ вас арканоид продолжает работать, даже когда блоки закончились :)
      • +1
        Это не игра, это демка ;)
        • 0
          Да я понимаю ) Решил пройти пока время было, обнаружил что демка бесконечная )
          • 0
            F5 и другой уровень))
  • 0
    Вся анимация в игре оптимизирована с помощью requestAnimationFrame, но разные устройства выдают разные показатели FPS.

    Это не оптимизация) Это просто небольшое удобство, которое не даёт существенного выиграша в скорости.
  • 0
    Ок, у меня в Фоксе 57 фпс. А вот есть ли какая-то реальная причина, почему в Опере 12.11 х64 не работает? Сейчас проверил, много других игр и примеров на Canvas отлично работают.
  • +3
    на 286м летала, а на i5 притормаживает
    а так прикольно, забавно и доставляет фана
  • 0
    *.mds — MIDI файлы музыки, сконвертировать их в *.mp3 мне помог savex.

    Выложите куда-нибудь, пожалуйста, отличные получатся рингтоны
    Можно и в midi
  • 0
    Музычка не закольцована. Закончилась и как-то одиноко стало.
  • +1
    всё ок но
    глюков много
    самопроизвольные переходы с уровня на уровень
    высокая, относительно оригинала, скорость шарика...*или мне кажется?*
  • 0
    1) В некоторых случаях в начале игры при запуске шарика оный внезапно исчезает, а затем теряется жизнь
    2) После прохождения первого уровня всегда вылетает критическая ошибка chrome «опаньки, что то пошло не так»
    Chrome 24.0.1312.0 (164656) Windows 7
  • 0
    По поводу определения столкновений стоит поправить — если два кирпича находятся один над другим и мячик попадает сбоку в их общую границу, он не может отскочить от верхней грани нижнего кирпича, т.е. столкновение должно обрабатываться как удар от вертикальную поверхность, а не горизонтальную.
  • 0
    Здорово! То дюна2 в браузере, теперь вот и DX-Ball… Месяц старых игр в новых браузерах? )
  • 0
    А в опере лучше не открывать!)
  • 0
    Это Мега игра!!!
    Помню коротал за ней не один вечер!
    Спасибо!
  • 0
    Заинтриговали ведь. Рассказывайте теперь, что секретные бонусы делают.
  • 0
    Странно, но DrWeb ругается на файл dx-ball.ru/game/game.js

    Причина: Данный объект может представлять угрозу (SCRIPT.Virus)
    Дата: ‎04.‎12.‎2012 ‏‎10:55
    • +3
      Это для офисных работников угрозу он представляет, сжирая всё рабочее время. Очень опасный скрипт.
  • 0
    Нужно управление кнопками. Срочно!

    И захват курсора мыши заодно, как уже писали. Спасибо.
    • 0
      Сделаем, но с начало надо избавиться от основных багов
  • 0
    Спасибо за игрушку моего детства :-) Небольшой баг-репорт настрочу вам…
    Насколько помню, в оригинале у каждого бонуса была некая частота появления. Например, реже всех появлялись бонусы окончания уровня и добавления жизни, а чаще всех — смерть. У вас хоть и чистый рандом, но жизни быстро увеличиваются и становится неинтересно играть.
    Частенько неправильно срабатывают коллизии на стыке двух кубиков. Наверное, вы убираете первый кубик сразу после проверки на коллизию с шариком, и в результате сразу отрабатывается и вторая коллизия тоже.
    Ну и не помню, как точно было в оригинале… но вроде бы «сквозные» шарики пролетали и через непробиваемые кирпичики.
    • 0
      По поводу бонусов, у нас не получилось вытащить вероятности выпадов, поэтому там чистый random(); конечно можно засесть часа на 3-4 и посчитать вероятности, но на это много времени и терпения надо.
    • 0
      Опытным путем было выявлено что, например, добавление жизни чаще всего падало из обределенных блоков на определённых уровнях. Т.е. Если в конкретный блок попасть шариком без бонусов в момент, когда не падает никакой другой бонус, то с большой вероятностью от туда выпадет +1.
      Когда шар в режиме кометы +1 почти никогда не падает. Как-то так.
  • 0
    Даёшь фулскрин!
    • +1
      F11 же =)
  • +1
    Скоро новый хаб под ремейки придется открывать! :)
    Спасибо!
  • 0
    У меня проявлялка (бонус проявляющий невидимые кирпичи и делающий непробиваемые пробиваемыми) глючит. Она сделала золотые кирпичи пробиваемыми, а розово-позалоченные, которые после удара становятся золотыми, оставила без изменений. Тем самым заблокировала мне 4й уровень.
  • 0
    На ZXSpectrum Арканоид называлась, я ее так запомнил)
    • 0
      не вы один, много где это арканойдом называется.
  • 0
    В Chrome 25.0.1337.0 dev-m падает в течение минуты — «упс»
    В IE 9.0.8112.16421 не стартует — чёрный квадрат малевича.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.