
Давным-давно, два английских школьника умудрились основать серию игр, ставшую легендарными играми для 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 для удаления выбранных тайлов. В целом, редактор не совсем доделан и имеет некторые особенности в работе с ним.
Выглядит интерфейс редактора вот так:

Вот такой вот получился движок для создания игр про Диззи. Теперь надо как-то придумать сценарий и сделать полноценную игру.
А пока, вот видео, как всё это работает:
Буду очень рад, если кому-либо пригодится этот движок. Быть может, кто-нибудь сможет на нём сделать свою игру про Диззи.
Дерзайте!
P.S. Та самая кассета с Диззи от студии MiM.
