Как я исполнил свою мечту и написал движок Диззи
Tutorial
Давным-давно, два английских школьника умудрились основать серию игр, ставшую легендарными играми для ZX-Spectrum. Да, речь про братьев Оливеров и их неподражаемого Диззи. Впервые услышал я про Диззи в начале девяностых в возрасте лет эдак девяти-десяти, когда мне рассказали, как подруга моей сестры играет в некую игру с бегающим и собирающим предметы яйцом на компьютере (!). Сам спектрум у меня появился чуть позже – в одиннадцать лет (это октябрь 1994 года), почти вместе с книжками серии «Как написать игру для ZX-Spectrum». И вот в книжке про написание игры на ассемблере была картинка из игры Dizzy-4. Увы, самой игры у меня не будет ещё год-два. Но всё-таки, в конце-концов, мне её купили, как сейчас помню, в ларьке в СПб на Балтийском вокзале. Кассета была известной многим студии “Михаил и Михаил” (MIM). Вот тогда-то я прочно запал на Диззи. Я играл в него с утра до вечера, разгадывая головоломки и собирая монеты. Много-много лет мне очень хотелось написать что-то подобное. В 1996 у меня даже получился невероятный примитив на бейсике. Много лет я методично приближался к своей цели. И вот именно сейчас, спустя 25 лет, у меня наконец-то получилось что-то более-менее играбельное. Вот о том, как написать такую игру, я и расскажу.
Сразу скажу, я понятия не имею, как устроен движок оригинального Диззи. Да и судя по играм серии на разных платформах, движки у них сильно разные даже на одной платформе, как разная и физика движения. В основе же моего движка – тайловая структура карты. То есть, вся карта собирается из кусочков 16x16 пикселей.
Выглядит это так:
Каждый тайл имеет маску проницаемости – такой же тайл, только с заливкой контура, где через этот тайл не может пройти главный герой. Используется эта маска только если тайлу назначено, что он является препятствием. Так же тайл может иметь атрибут переднего плана. В этом случае, он выводится перед Диззи и закрывает его собой. Ещё есть атрибут рисования тайла поверх фона, но за Диззи. Этот атрибут позволяет, скажем, сделать двигающийся тайл, выводящийся поверх тайлов фона (облаков, строений, ландшафта). Тайлы могут иметь заданную последовательность кадров анимации. Всего у меня доступно три типа анимации – анимация с заданием текущего кадра, циклическая анимация, однократная анимация. Задание текущего кадра позволяет переключать картинку по ситуации. Однократная анимация может применяться для растворения каких-то объектов-препятствий или, наоборот, для появления чего-либо. Для взаимодействия с тайлами им можно назначать имена.
Предметы, которыми оперирует Диззи, тоже точно такие же тайлы, как и всё остальное. В движке вообще всё является тайлами.
Взаимодействие между тайлами осуществляется с помощью условий и действий над участвующими в условиях тайлах (в некоторых действиях участвуют все тайлы карты).
Для написания условий игры у меня используется простейший интерпретатор, создающий цепочки классов взаимодействия, и понимающий следующие команды.
Возможны следующие условия:
- Пересечение тайла с Диззи: IfDizzyIntersection(«CAT») — данное условие сработает при столкновении с тайлом с именем «CAT».
- Отсутствие пересечения тайла с Диззи: IfNotDizzyIntersection(«CAT») — данное условие сработает при отсутствии столкновении с тайлом с именем «CAT».
- Пересечение тайлов между собой: IfIntersection(«FIRE_LEFT»,«FIRE_LEFT_BORDER») — данное условие сработает при столкновении тайла «FIRE_LEFT» с тайлом с именем «FIRE_LEFT_BORDER» (это движущийся влево огонь и граница его перемещения).
- Взятие тайлов в инвентарь (да, Диззи нужно отдельно разрешать что-то брать — могут быть неберущиеся предметы, как это было с мечом в камне из Диззи-4 — взять его можно только используя «липкие руки»): IfPickUp(«RING») данное условие сработает при попытке взять тайл «RING».
- Срабатывание таймера: IfTimer(«WAIT CAT») — данное условие сработает для тайла «WAIT CAT „при срабатывании таймера.
- Использование тайлов между собой: IfUse(“BOTTLE WATER»,«CAT») — данное условие сработает при взаимодействии тайла с именем «BOTTLE WATER» на тайле «CAT».
На данный момент это все возможные условия. Потом, может быть, появятся новые.
Когда условие сработало, выполняются какие-то действия (Action). Для некоторых условий действия только для одного тайла, а для других нужно описывать действие для двух тайлов ( скажем, при столкновении нужно задать каждому столкнувшемуся, что произойдёт с ним)
Этих действий много. Сейчас они такие:
- ActionMessage(20,100,«СООБЩЕНИЕ») — будет выведено сообщение в заданных координатах. Да, экран в моём Диззи 320x240, растянутый до 640x480 для PC.
- ActionChangeName(«BOTTLE OF WATER») — поменять тайлу имя на заданное. Зачем это нужно? Бежал у вас огонь до границы влево и теперь должен бежать вправо. Как это сделать? Поменять ему имя. И для другого имени сделать уже условие контроля правой границы и условие таймера с событием изменения координаты в другую сторону.
- ActionChangeDescription(«БУТЫЛКА ВОДЫ») — заменяет описание предмета, которое выводится в инвентаре. Была у вас бутылка пустая, а стала с водой. Имя вы поменяли. А теперь надо описание для инвентаря поменять.
- ActionChangeGlobalName(«BOTTLE OF WATER») — поменять имя для ВСЕХ тайлов с таким же именем на карте. Зачем нужно? Если картинка состоит из ряда тайлов (скажем, фигура Волшебника), то изменяет его состояние все его тайлы, а не только та часть, с которой вы взаимодействовали.
- ActionChangeGlobalDescription(«БУТЫЛКА ВОДЫ») — так же меняет глобально все описания.
- ActionChangePosition(100,100) — задать тайлу позицию в числах. Неудобно для использования. Не гибко.
- ActionCopyPosition(«RING»,«RING_POS») — перенести позицию первого тайла на место второго. Например, когда кот вам даёт кольцо, он переносит кольцо из некой области карты (вам не видимой — вы там не будете гулять) в заданную позицию.
- ActionPickUp() — добавляет тайл в список возможных для взятия в инвентарь.
- ActionSingle() — однократное действие. Зачем нужно? Диззи коснулся тайла воды. Должен терять энергию. Но вот беда, коснулся он нескольких тайлов воды. Совершенно незачем для каждого тайла отнимать у Диззи энергию. Вот это действие и выполнит для данного события действия стоящие следом ровно один раз.
- ActionSetAnimationStep(1) — устанавливает кадр анимации (анимация, обычно, в этом случае есть, но в редакторе выбран режим анимации по кадрам). Позволяет менять картинку одного тайла на другой. Была бутылка без воды, стала с водой.
- ActionMove(1,0) — изменяет координату тайла на заданные приращения по X и по Y. Именно с помощью этого действия и движется, например, тайл огня.
- ActionSetEnabled(true) — задаёт разрешён тайл или нет. Если нет, он удаляется с игрового поля. Так можно избавляться от ненужных предметов и персонажей.
- ActionEnergyUpdate(-1) — изменяет энергию Диззи.
- ActionAddScore(100) — изменяет очки Диззи. Кстати, можно и уменьшать.
- ActionAddLife() — добавляет Диззи жизнь.
- ActionAddItem() — увеличивает счётчик найденных предметов на 1 (в Диззи-6 Диззи собирал вишенки, например).
- ActionCopyPositionOffset () — перенести позицию первого тайла на место второго со смещением.
- ActionChangeAnimationMode() – меняет режимы анимации. Например, была у вас анимация с задание кадра, а стала однократная.
Начало и конец блоков действий описывают (в зависимости от действия) ключевыми слоами
ActionBegin
ActionEnd
или
ActionFirstBegin
ActionFirstEnd
ActionSecondBegin
ActionSecondEnd.
Есть ещё команды, выполняемые до начала игры и к действиям и условиям не относящиеся:
- SetDescription(«BOTTLE WATER»,«БУТЫЛКА ВОДЫ»)- задать описание.
- CopyPosition(«FIRE»,«FIRE_POS») — перенести тайл в позицию другого тайла.
- CopyPositionOffset(«FIRE»,«FIRE_POS») — перенести тайл в позицию другого тайла со смещением.
- SetDizzyPosition(«DIZZY_START_POSITION») — перенести Диззи в позицию тайла. Позволяет задать место старта.
Особый тайл имеет имя «RESPAWN» — его нужно ставить там, где Диззи может погибнуть. Тогда Диззи возродится у ближайшего такого тайла.
А вот пример сценария:
IfUse("BOTTLE WATER","WAIT CAT")
ActionFirstBegin
ActionChangeGlobalDescription("ПУСТАЯ БУТЫЛКА")
ActionChangeGlobalName("BOTTLE")
ActionSetAnimationStep(0)
ActionFirstEnd
ActionSecondBegin
ActionSingle()
ActionChangeGlobalName("LUCKY CAT")
ActionCopyPosition("RING","RING_POS")
ActionMessage(30,100,"ДИЗЗИ ДАЛ БУТЫЛКУ ВОДЫ КОТЁНКУ...")
ActionMessage(40,80,"БУЛЬК-БУЛЬК!\СПАСИБО! ЗА ЭТО Я ДАМ ТЕБЕ\КОЛЬЦО. Я ЕГО ГДЕ-ТО СПЁР.")
ActionAddScore(100)
ActionSecondEnd
Здесь при использовании бутылки с водой на ждущем котёнке, бутылка становится пустой, а котёнок счастливым, после чего котёнок переносит кольцо из какой-то скрытой от игрока области в заданный тайл, выводит сообщения и добавляет Диззи очков.
Кстати, для использования предметов Диззи выкладывает предмет, проставляет ему свои координаты, а затем уже проверяются условия использования одного тайла на другом. Если использовать не удалось или предмет использовался, но не пропал (бутылка с водой стала просто бутылкой), предмет возвращается в инвентарь.
Или вот:
IfDizzyIntersection("FIRE_LEFT")
ActionBegin
ActionSingle()
ActionEnergyUpdate(-1)
ActionEnd
Здесь уже написано, что при пересечении с огнём, Диззи должен терять энергию.
При запуске движок сканирует папку ScreenPlay и все найденные текстовые файлы обрабатывает как файлы сценариев, добавляя в игру.
Что касается физики работы движка, то я использую следующий подход.
Диззи имеет координаты относительно левого верхнего угла экрана X и Y и их приращения в текущий момент времени dX и dY. Так же у Диззи есть спрайты полоски взаимодействия ног и форма тела без ног (она нужна, чтобы с ней сравнивать, может Диззи подняться на преграду или нет).
Диззи хранит куда он идёт или прыгает: идёт влево, идёт вправо, прыгает на месте, прыгает влево, прыгает вправо (режим Move задан для каждого кадра анимации Диззи, сами кадры последовательности, ссылающиеся на следующий кадр анимации и движения).
Если под Диззи нет твёрдой поверхности, игроку отключается управление (MoveControl=false).
Если Диззи коснулся твёрдой поверхности из прыжка, но прыжок не закончен, Диззи продолжает движение в горизонтальной плоскости с имеющейся скоростью. Это даёт так раздражающие, иногда, перекаты после прыжка и является, собственно, тем, за что Диззи назвали Диззи.
Есть так же координаты левого верхнего угла экрана в пространстве карты Map_X и Map_Y.
Итак, сначала рисуется маска проницаемости тайлов, отмеченных как препятствие, через который Диззи пройти не может.
А дальше делается так
int32_t step_x=abs(dX);
int32_t step_y=abs(dY);
int32_t dx=dX;
int32_t dy=dY;
while(step_x>0 || step_y>0)
{
if (step_x>0) step_x--;
if (step_y>0) step_y--;
int32_t last_x=cGameState.X;
int32_t last_y=cGameState.Y;
if (dx>0) cGameState.X++;
if (dx<0) cGameState.X--;
if (IsCollizionLegs(iVideo_Ptr,cGameState.X,cGameState.Y)==true || IsCollizionBody(iVideo_Ptr,cGameState.X,cGameState.Y)==true)//зафиксировано столкновение
{
if (IsCollizionBody(iVideo_Ptr,cGameState.X,cGameState.Y)==false)//пересечение не выше допуска
{
//поднимаем Диззи на уровень без пересечения
while(IsCollizionLegs(iVideo_Ptr,cGameState.X,cGameState.Y)==true) cGameState.Y--;
}
else
{
cGameState.X=last_x;
dx=0;
//dX=0;//если так сделать, Диззи не сможет забираться, перекатываясь через края блоков.
}
}
if (dy>0) cGameState.Y++;
if (dy<0) cGameState.Y--;
if (IsCollizionLegs(iVideo_Ptr,cGameState.X,cGameState.Y)==true || IsCollizionBody(iVideo_Ptr,cGameState.X,cGameState.Y)==true)//зафиксировано столкновение
{
cGameState.Y=last_y;
dy=0;
dY=0;
}
bool redraw_barrier=MoveMapStep(width,height,offset_y);
if (redraw_barrier==true)
{
iVideo_Ptr->ClearScreen(NO_BARRIER_COLOR);
DrawBarrier(iVideo_Ptr);
}
}
if (IsCollizionLegs(iVideo_Ptr,cGameState.X,cGameState.Y+1)==false)//можно падать
{
if (MoveTickCounter==0)
{
if (dY<SPEED_Y) dY++;
}
if (dY>0) cGameState.Y++;
MoveControl=false;
}
else
{
if (cDizzy.sFrame_Ptr->Move==CDizzy::MOVE_JUMP_RIGHT || cDizzy.sFrame_Ptr->Move==CDizzy::MOVE_JUMP_LEFT)//режим прыжка должен завершиться
{
if (cDizzy.sFrame_Ptr->EndFrame==true) MoveControl=true;//перекатывание завершено
}
else MoveControl=true;
}
//особый случай: Диззи не двигался, но произошло столкновение (так как двигался другой элемент)
if (IsCollizionLegs(iVideo_Ptr,cGameState.X,cGameState.Y)==true || IsCollizionBody(iVideo_Ptr,cGameState.X,cGameState.Y)==true)//зафиксировано столкновение
{
//предмет вытесняет Диззи вверх
for(size_t n=0;n<TILE_WIDTH/4;n++)
{
if (IsCollizionLegs(iVideo_Ptr,cGameState.X,cGameState.Y)==true || IsCollizionBody(iVideo_Ptr,cGameState.X,cGameState.Y)==true) cGameState.Y--;
}
}
}
С такими настройками Диззи прыгает довольно канонично.
Поиграть в прототип игры можно вот тут.
Прототип редактора карт можно взять тут
В редакторе используются в режиме выбора клавиши insert для задания последовательности анимации и delete для удаления выбранных тайлов. В целом, редактор не совсем доделан и имеет некторые особенности в работе с ним.
Вот такой вот получился движок для создания игр про Диззи. Теперь надо как-то придумать сценарий и сделать полноценную игру.
А пока, вот видео, как всё это работает:
Буду очень рад, если кому-либо пригодится этот движок. Быть может, кто-нибудь сможет на нём сделать свою игру про Диззи.
Дерзайте!