Всем привет. В этой публикации хочу рассказать о том, как делал игру под операционную систему KolibriOS, о существовании которой раньше даже и не подозревал.
Как всё началось?
А началось всё с того, на igdc.ru появилось объявление от администрации KolibriOS о начале конкурса на создание игры под их операционную систему.
Сперва я не принял это всерьез и не хотел участвовать по нескольким причинам. Главной, конечно же, является лень. Да-да, она спасает от многого безумия в моей жизни. Попыталась спасти и в этот раз, но одним зимним днем, когда шел с работы, на меня снизошло озарение, лампочка загорелась адским пламенем и взорвалась над моей головой. Я понял, что это хороший шанс узнать что-то новое и я просто обязан сделать игру на этот конкурс.
Конечно, я надеялся, что добравшись домой забью на эту идею (как это обычно и бывает), но, на мое удивление, этого не случилось. Даже наоборот. Когда я пришел домой и установил KolibriOS на виртуальной машине, загорелся этой идеей настолько, что впору уже тушить. Инструменты немного поумерили пыл.
Инструменты
Подготовка к написанию игры началась с выбора языка и среды разработки. По началу взгляд пал на С, но отсутствие ООП делает меня грустным пандой. Вариант использовать Ассемблер… Не рассматривался. Довольно быстро был найден вариант написания «Hello World» на С++, и это меня вполне устраивало. Правда, немного мешала непереносимая ненависть к С++, но борьба с ней помогла таки понять тайну звездочек в языке.
Написав пару примеров по разным статьям, я так и не смог скомпилировать приложение. Пришлось обращаться за помощью к экспертам на форуме. Добрые люди не заставили долго ждать и быстро наставили меня на путь истинный.
Остановился на утилите автоматического создания проекта под Visual Studio под компилятором FASM — это один из самых простых способов, при котором компиляция из среды сразу же создавала запускаемый файл приложения для Kolibri. Указав путь компиляции на USB флешку, стало удобно запускать приложение на виртуалке. Способов проще не нашлось, общие папки не поддерживаются.
На сборку пустого проекта было потрачено около недели, поэтому, несмотря на то, что на конкурс давалось почти 2 месяца, на создание игры осталось всего 20 дней. Но всем известно — чем ближе дедлайн, тем выше продуктивность.
12-20 Декабря
Настал сокровенный момент выбора, какую же игру делать в доступных технологических условиях. Сперва я хотел сделать скроллер, но первые же эксперименты с выводом картинок убили эту идею. Отсутствие аппаратного ускорения и проблемы с точными таймерами не позволяли малой кровью сделать динамичную игру. Вывод картинки осуществляется принципом передачи одномерного массива RGB цветов пикселей, указания размеров изображения и позиции.
struct RGB
{
Byte b;
Byte g;
Byte r;
}
…
void kos_PutImage( RGB * imagePtr, Word sizeX, Word sizeY, Word x, Word y);
Графика в игре будет спрайтовая, а значит, надо загрузить изображение из файла, но какой формат использовать? png, jpg или gif? Как всегда выбор пал на png, он бесплатный, довольно экономичный, не портит картинку сжатием и имеет возможность хранить альфа-канал. Поработав с чтением файлов в KolibriOS, я наступил на десятки граблей и решил использовать простой массив пикселей, занесенных в глобальную константу.
const RGB img_water[576] = {
0x0B79BD, 0x0A6DAE, 0x0A69AA, 0x0A6EAD, 0x0B71B0, 0x0A65A8, 0x0A65A6, 0x0B75B4, 0x0B6DAD, 0x0B71B1, 0x0A6AAD, 0x0A6DAD, 0x0C7EBB, 0x0A66AB, 0x0B66AD, 0x0B6DB1, 0x0A61A3, 0x0B79B7, 0x0A72B1, 0x0A6AAD, 0x0C85C1, 0x0A6AAB, 0x0A62A3, 0x0B6EB2, 0x0B7ABC, 0x0B6BAC, 0x0C8BC3, 0x0C9BCE, 0x0C88C1, 0x0B75B4, 0x0B81BD, 0x0B89C1, 0x0B71B1, 0x0B7AB8, 0x0A74B1, 0x0B76B5, 0x0B86BF, 0x0B81BC, 0x0B81BC, 0x0A5B9F, 0x0B70AF, 0x0C86BE, 0x0B76B5, 0x0C94C6, 0x0D9DCB, 0x0A6EAF, 0x0B70B0, 0x0B70B4, 0x0B72B2, 0x0A6EAE, 0x0C8BC4, 0x0DA1D3, 0x0C8CC9, 0x0B7CB9, 0x0B7DBA, 0x0B70AF, 0x0B7CB9, 0x0B89C2, 0x0B80BB, 0x0B7AB9, 0x0B80BD, 0x0C9FCC, 0x0B8DC1, 0x0B73B3, 0x0B79B6, 0x0A61A4, 0x0B81BB, 0x0DAAD3, 0x0EB7D8, 0x0C86C4, 0x0B80BC, 0x0B79BA, 0x0A6EAD, 0x0B81BC, 0x0B8DC5, 0x0C94CA, 0x0C8AC8, 0x0C8DC4, 0x0E90C6, 0x0A64A5, 0x0B71B0, 0x0B81BD, 0x0B87C0, 0x0C8AC6, 0x0D90CB, 0x0D9FCC, 0x0B84BD, 0x0B77B7, 0x0B7DBA, 0x0B80BB, 0x0C97C8, 0x0DA6D3, 0x0EC8E0, 0x0D94CB, 0x0C8AC4, 0x0B79B9, 0x0A64A6, 0x0B7EBA, 0x0B86BF, 0x0B7EBA,
…
};
И вот, наконец, получилось вывести картинку в окне приложения.
Но оказалось, что в структуре пикселя отсутствует прозрачность. Это вставило ноги мне в рот, а в колёса палки. Возникла идея создания своего буфера кадра. Оказалось, что его использование довольно требовательно ресурсам системы и при выводе буфера возникает мерцание. Для борьбы с этими минусами было решено использовать RenderTarget, в которых фон попиксельно смешивался со спрайтом объекта, имеющего альфу.
alpha = (float)addPixel.a / 255.0f;
newPixel.r = pixel.r * (1 - alpha) + addPixel.r * alpha;
newPixel.g = pixel.g * (1 - alpha) + addPixel.g * alpha;
newPixel.b = pixel.b * (1 - alpha) + addPixel.b * alpha;
Но и тут было несколько проблем. Пришлось создать свою структуру пикселя ARGB (с прозрачностью) и ряд функций по работе с массивами изображений. Вывод спрайтов с альфой готов; но теперь я с ужасом осознал, что понятия не имею, что за игру буду делать.
21-22 Декабря
Сегодня мой взгляд упал на пункт правил конкурса об автосборке проекта. Прочитав пару статей на эту тему, я запутался и испугался еще больше, поэтому опять обратился к форуму за помощью. Но ответа ждать не стал и сразу приступил к разработке самой игры. Основа геймплея была взята из старого прототипа на конкурс IGDC.ru №94, в котором я участвовал пару лет назад. Как вспоминаю тот проект, так вырывается непроизвольная улыбка. В этой конкурсной работе был просто небольшой стёб над администрацией и конкурентами конкурса: я написал игру в Delphi кириллицей со всеми принципами хорошего тона при формировании кода. Но это совсем другая история.
type
Логика = Boolean;
Число = Integer;
Дробь = Single;
ТикТаймера = Double;
Точка = TPoint;
TСписок = TList;
const
ПРАВДА: Логика = True;
ЛОЖЬ: Логика = False;
…
type
TЛазер = record
X, Y, Угол, Тайл: Число;
end;
TИгрок = class(TОбычныйОбъект)
strict private
FЛазер: array of TЛазер;
FКнопки: array[0..255] of Логика;
procedure ДелаемЛазер(Время: ТикТаймера);
public
procedure Процесс(Время: ТикТаймера); override;
procedure Рисуй(Слой: Число); override;
procedure ЗажатаКнопка(Кнопка: Число);
procedure ОтжатаКнопка(Кнопка: Число);
function ПроверкаСмещения(X, Y: Число): Логика; override;
end;
…
while (Карта.Тип[Тчк.X, Тчк.Y] in [тмТрава, тмВода, тмЯщикВВоде]) do
begin
…
if (Карта.Объект[Тчк.X, Тчк.Y] is TЯщик) or (Карта.Объект[Тчк.X, Тчк.Y] is TПушка) then
begin
Карта.Объект[Тчк.X, Тчк.Y].СместитьПоВектору(Смещение.X, Смещение.Y);
Конец := ПРАВДА;
end
Но вернемся назад к нашим баранам.
Для работы с графикой была написана небольшая программа на Delphi, позволяющая преобразовывать изображение png в C++ массив пикселей. Это дало хороший толчок к дальнейшему развитию. Как итог первый прототип уровня был сделан довольно быстро:
Вообще, в связи с небольшим количеством встроенных средств, пришлось делать функции для самых базовых операций с графикой и логикой игры.
В игре сразу была запланирована плавная анимация перемещения по уровню -— никаких прыжков по клеточкам и дерганий.
Честно говоря, тут я решил немного сжульничать, внося изменения только при действиях игрока. Это значительно разгрузило систему и при отрисовке, и в моменты простоя. Для поворота танка было решено не делать заранее заготовленные изображения, а написать алгоритм для поворота изображения. Обычное отражение по горизонтали и вертикали с переменой координат дало простой поворот картинки на угол, кратный 90 градусов. Первое время этого хватало с лихвой. Но никак не стыковалось с плавностью движения самого танка. Чуть позже сделано будет и это, но сначала сделаем толкание ящиков и сбрасывание их в воду для создания моста. Это позволит заниматься не спецэффектами, а непосредственно механикой игры.
23-24 Декабря
Посмотрев на то, как выглядит игра, невольно пришла идея заменить графику неясного происхождения на свою, во избежание проблем с авторством. Пришлось искать художника (но, надо сказать, недолго) и заказывать у него графику. Взгляд пал на знакомого художника Weilard. Обсудив идею и показав прототип, мы договорились о цене и начали работу. (вы же не думаете, что профессионалы работают бесплатно?)
На следующей день он уже предоставил мне пару эскизов своего видения игры:
Четвертый вариант показался мне наиболее стилистически верным, да и по цветам понравился больше, поэтому, выбрав четвертый вариант, я попросил изменить цвет кирпича и получил почти конечный вариант:
Вырезав кусочки из эскиза, сразу же воткнул их в игру вместо моих картинок. Остальную графику мастер обещал предоставить в выходные (27-28 декабря). С этой графикой открылось второе дыхание и вся усталость ушла прочь. Я просто жил этой игрой.
Возвращаясь к проблеме резкого поворота, скажу, что как раз тут настал тот момент, когда то, что танк резко поворачивается на 90 градусов, портило всю картинку. Нужно было срочно избавится от этого, пришлось переписывать алгоритм отрисовки с учетом поворота на любой угол. Получилось здорово. Никогда бы не подумал, что буду писать алгоритм попиксельного поворота изображения.
Следующим шагом был реализован лазер. Алгоритм прост — рекурсивно идем по клеточкам уровня по направлению вектора движения, пока не столкнемся с препятствием. Если препятствие — зеркало, проверяем его ориентацию и либо заканчиваем цикл (зеркало повернуто к нам стенкой), либо меняем вектор направления и идем дальше (попали в само зеркало).
Попробовав снова вернуться к автосборке и изучив этот вопрос, решил, что лучше отложить до выходных, ведь очень хотелось закончить геймплей. Он сейчас был куда важнее, иначе сдавать будет совсем нечего.
25-26 декабря
После добавления разрушения кирпичной стены лазером проект стал компилироваться достаточно долго. Причина скрывалась в большом количестве глобальных констант, которыми были забиты в код текстуры. Сделав загрузку ресурсов из файла, я поборол эту проблему. Возится с загрузкой png-файлов не было времени и желания, так как формат с сжатием и требует приличного загрузчика. Поэтому пришлось быстренько переделать свою утилиту по созданию массива пикселей в файл. Я потратил уйму времени пока понял, что нужно указывать полный путь к файлу (относительные пути не поддерживаются), но совершенно случайным образом удалось это сделать. т.к. файл с текстурой был простым массивом RGB или ARGB структур, я решил не заморачиваться и сделал утилиту по упаковке всех файлов в один.
Убрав из кода все глобальные массивы, проект стал компилироваться моментально, что меня несказанно радовало.
27 Декабря
И опять эта автосборка… Автосборка получилась не сразу. Благо добрые люди на форуме помогли избавить проект от ошибок и собрать дистрибутив KolibriOS с вшитой игрой. Тем же днем художник наконец-таки скинул фоны для меню, окна выбора уровней и дополнительные текстуры объектов. Если раньше в игре была только одна сцена (разумеется, речь о режиме игры), то для внедрения меню, паузы и выбора уровня в проект пришлось добавлять еще сцены. Сперва появилось меню с кнопками начала игры и выхода. При создании перехода между сценами пришлось сперва реализовать события победы и поражения, но тут встала проблема: уровень был только один, и тот хранился в константе глобального массива.
Как всегда, пришла на помощь Delphi, которая помогла, быстро, накидать скромный редактор уровней:
Я опять немного увлекся этим делом и сделал супер-редактор, простой и удобный. Хотя и плюс в этом есть — теперь любой желающий, кому не лень, может открыть редактор и добавить парочку своих уровней в игру:
28-29 Декабря
Следующие пару дней я посвятил добавлению лазерных пушек и их уничтожению. Проверку на попадание танка в поле зрение пушк поставил на момент окончания движения танка. Чтобы найти активную пушку на линии огня, от танка по горизонтали и вертикали пускались проверки клеток на наличие пушки, повернутой в нашу сторону. Если она находилась, то запускался тот же алгоритм лазера, что и у танка, но с другим цветом. Тут пришлось снова изменить алгоритм отрисовки в буфер, чтобы можно было делать покраску текстуры в указанный цвет.
Уничтожение пушки было возможно только при попадании лазером ей в дуло, но простое визуальное исчезновение мне не нравилось. Взяв в руки Magic Particles, я сделал кадровую развертку анимации взрыва и еще раз доработал отрисовку спрайта в буфер. Теперь уже с учетом номера кадра, которых можно стало хранить в текстуре несколько.
Также заказал художнику нарисовать еще один тайл, пятно от взрыва (декаль).
Прикинув, что дедлайн совсем скоро, в голову пришла идея сделать два вида зеркал (статичные и передвижные), а также пропускающие лазер стенки. Убитый во мне художник ожил и воспрянул — взяв текстуры стенки и ящика, я порезал их пополам и склеил с зеркалами. Получилось новое геймплейное решение, которое помогает легко определить можно передвигать зеркало или нет.
30 Декабря
Ну вот и пришло время для создания сцены с выбором уровня. Я не мог не учесть тот момент, что редактором карт любой желающий может добавить или удалить уровень. Поэтому сделал постраничную навигацию по всем уровням в файле, тридцать уровней на странице. На данный момент в игре есть лимит на количество уровней. Меньше тысячи уровней, и то, только из-за того, что четырехзначное число не влезает в квадратик с кнопкой :D.
Наступила ночь на 31 число и почти все баги уже превратились в фичи. Как хорошо, что я взял отпуск на 3 последних дня, это позволило пройтись рубанком и напильником по проекту. Кстати, один из забавных багов: после лазера под зеркалами менялась почва, на ту, что под танком (вторая гифка).
31 Декабря
Решил посвятить весь день созданию уровней, ведь геймплейная часть уже была вполне готова. Это оказалось самым сложным во всей разработке. На создание 48 уровней было потрачено около 9 часов.
До конца сдачи остаётся три часа, я делаю коммит и успокаиваю нервы. Всё, успел!
Исходники
Исходники на SVN: LaserTank
Обсуждение игры на форуме: board.kolibrios.org/viewtopic.php?f=41&t=2934
Голосование
Посмотреть другие конкурсные работы и проголосовать за понравившиеся можно тут, на хабре.
Автор: ZblCoder