Войти
ПрограммированиеСтатьиОбщее

Своя 2D инди игра на Delphi (7) без DirectX - это просто!

Внимание! Этот документ ещё не опубликован.

Автор:

Статья будет пополняться по частям и не сразу (извините, но создание полноценной статьи очень тяжкий интеллектуальный труд.)

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

Часть I.
Введение.
Некоторые программисты Delphi (у меня тоже такое было) иногда жалеют, что они решили осваивать именно эту среду разработки, а не более актуальные такие как C++ или игровые движки вроде Unity. От этого у них возникает вопрос "Так стоит ли вообще начинать разрабатывать игру на Object Pascal? Может начать изучать что-то другое?". Отвечу сразу - определенно стоит. Начать осваивать что-то новое вы всегда успеете, как говорится - нет предела совершенству. А ваши накопившиеся знания Delphi не должны пропасть даром. Да, вполне возможно C++ более гибок и документации по созданию игр на нем намного больше, но я вас уверяю, с помощью Delphi тоже можно получить отличный результат. Один из ярких примеров - это игра Soldat 2D. Ведь главное в разработке игры - это все таки результат, а не способ его достижения (но он тоже очень важен).

С чего же начать?
Самое главное при старте разработки вашего проекта (если основная идея уже давно пылится в вашей голове) является не выбор версии Delphi или создание папки вашего проекта на жестком диске, а ваша команда. Скажу сразу - программировать в одиночку - очень тяжкое дело. В интернете полно способов как все-таки не забросить проект - разрабатывать сразу несколько проектов, или насильно заставлять себя доделывать до конца, но самый действенный - это, как минимум, один ваш напарник, даже если он ВООБЩЕ не разбирается в программировании, но, желательно, чтобы он тоже был очень заинтересован в вашем ОБЩЕМ проекте. Ведь если у вас есть такой напарник, то, как минимум, сама мысль оставить проект будет уже менее приятной, т.к. есть тот, кто на вас надеется и вы не можете его вот так просто подвести. Но самое главное, что этот человек будет помогать вам в остальном - дизайне, создании карт на вашем редакторе и просто источником сил для продолжения проекта. Одна голова хорошо - а две ещё лучше!

А теперь начнем.
Вот вы уже выбрали версию Delphi (оставлю выбор за вами, т.к. считаю это не таким важным моментом), создали папку для проекта, и я надеюсь, что вас как минимум двое. Теперь мы разберемся, что самое главное в игре? На этот вопрос нет общего ответа, т.к. каждому в каждой игре нравится что-то своё. Кому-то нравится сюжет, кому-то графика, кому-то музыка. Но при создании игры (а точнее игрового движка) все-таки одним из главных моментов - это разработка графической составляющей. Ведь игра - это в первую очередь компьютерная программа, которая отображает всю информацию на мониторе, а как известно, человек воспринимает более 90% информации с помощью своего зрения. Поэтому в данной статье я постараюсь наиболее полно рассказать о внедрении графики в свой проект, а остальное (звук например) я оставлю на вас.
Так что же может послужить графическим ядром (кроме DirectX) нашего проекта? На этот вопрос тоже очень много ответов в интернете, но я заострю внимание в данной статье на библиотеке (а точнее наборе модулей) FastLIB.

Рассмотрим плюсы и минусы данной библиотеки:
+ Простота использования (по сравнению с DirectX в десятки раз);
+ Отличная скорость прорисовки (fps может доходить до 500);
+ Набор всех необходимых функций (альфа-каналы, масштаб, повороты ...);
+ Возможность создания любых своих спец-эффектов;
+ Открытый код для возможности модификации библиотеки;
Но есть один БОЛЬШОЙ минус:
- Вся нагрузка приходится на процессор и ни единого намека на использование видео-карты;

Но не стоит бояться этого минуса, ведь даже на слабеньких старых процессорах (у меня старенький одно-ядерный Pentium 4 - 3000 Гц) при нескольких тяжелых спец-эффектах частота держит норму в 60 fps. А большая (намного большая) часть пользователей уже давно имеет несколько-ядерные машины.

Внедрение FastLIB в проект.
Так как же использовать библиотеку FastLIB? На самом деле самое сложное, с чем я в ней сталкивался, это включение альфа-канала. Для начала скопируйте все модули библиотеки в папку с проектом и включите их в проект через Delphi любым привычным способом (эту задачу я снова оставлю на вас). После этого можете открыть и изучить самый главный модуль - FastDIB. В нем описан самый главный класс - TFastDIB. По сути дела это тот-же TBitmap, но направленный на получение большей скорости при отрисовки. И все функции библиотеки используются именно к этому классу TFastDIB. Далее, что бы мы не использовали - будь то текстура, или спрайт, или поверхность, она обязательно будет основываться на классе TFastDIB.

Часть II.
Основные функции класса TFastDIB.

Так как класс TFastDIB является объектом, то его необходимо создавать при начале и уничтожать в конце работы. Осуществляется это при помощи функций Create и Destroy. Создавать объект необходимо сразу как возникает необходимость работы с ним, но только один раз, иначе создастся новый объект, а прошлый потеряется в памяти компьютера. Уничтожать так же следует только после того, как объект уже не используется и никогда уже не будет использован.

После того, как объект был создан, можно его отрисовывать на любую поверхность. Для отрисовки в классе TFastDIB достаточно много функций (разные способы отрисовки), но самой элементарной является функция Draw.

Пример создания, отрисовки и уничтожения переменной типа TFastDIB:

var sprite: TFastDIB;
  ...
  // создание объекта
  sprite := TFastDIB.Create;
  ...
  // манипуляции с объектом
  ...
  // отрисовка на форму
  sprite.Draw(Form1.Canvas.Handle,0,0);
  // уничтожение объекта
  sprite.Destroy;
  ...

В данном примере мы отрисовываем sprite на дескриптор поверхности окна в координатах 0x0, но в коде нет примера, который бы заполнял наш спрайт данными. Т.е. мы на самом деле ничего не получим на нашем окне. Для того, чтобы что-то нарисовать, нужно чтобы было что рисовать. На этот случай есть два варианта - либо мы сначала что-то рисуем на самом sprite, либо мы загружаем картинку из файла с помощью функции LoadFromFile.

Пример создания, загрузки, отрисовки и уничтожения переменной типа TFastDIB:

var sprite: TFastDIB;
  ...
  // создание объекта
  sprite := TFastDIB.Create;
  // загрузка из файла
  sprite.LoadFromFile('picture.bmp');
  ...
  // манипуляции с объектом
  ...
  // отрисовка на форму
  sprite.Draw(Form1.Canvas.Handle,0,0);
  // уничтожение объекта
  sprite.Destroy;
  ...

Примечание: с помощью этой функции можно загружать графические данные только формата *.bmp.

Так же предусмотрена возможность сохранения результата с помощью функции SaveToFile.

Рассмотрим все возможные способы отрисовки и их функции:
Draw(fDC:HDC;x,y:Integer) - копирует содержимое TFastDIB на дескриптор fDC в координатах [x;y];
Stretch(fDC:HDC;x,y,w,h:Integer) - растягивает содержимое TFastDIB на дескриптор fDC в координатах [x,y] в ширину w и высоту h;
TransDraw(fDC:HDC;x,y:Integer;c:TFColor) - копирует содержимое TFastDIB на дескриптор fDC в координатах [x;y] кроме пикселей с цветом c;
TransStretch(fDC:HDC;x,y,w,h:Integer;c:TFColor) - растягивает содержимое TFastDIB на дескриптор fDC в координатах [x,y] в ширину w и высоту h кроме пикселей с цветом c;
AlphaDraw(hDC,x,y,a,hasAlpha) - копирует содержимое TFastDIB на дескриптор fDC в координатах [x;y] с непрозрачностью a с поддержкой альфа-канала (hasAlpha = true) и без поддержки альфа-канала (hasAlpha = false);
AlphaStretch(fDC:HDC;x,y,w,h:Integer;a:Byte;hasAlpha:Boolean) - растягивает содержимое TFastDIB на дескриптор fDC в координатах [x,y] в ширину w и высоту h с непрозрачностью a с поддержкой альфа-канала (hasAlpha = true) и без поддержки альфа-канала (hasAlpha = false);
TileDraw(fDC:HDC;x,y,w,h:Integer) - заполняет область поверхности с дескриптором fDC в координатах [x;y] размером w на h;

Использование альфа-канала.
Одним из важных моментов каждой игры (если рассматривать только графическую составляющую) является наличие красивых спец-эффектов, будь то эпичные взрывы или стеклянный интерфейс. Для этого используют полупрозрачные спрайты, а именно спрайты 32-х битного режима. Это значит, что на один пиксель спрайта выделяется 32 бита памяти, а именно 4 байта - r,g,b и альфа-канал. Каждый из байт может содержать значение от 0 до 255. Таким образом альфа-канал - это степень непрозрачности каждого пикселя от 0 до 255. Ниже будет приведен пример создания спрайта с использованием альфа-канала и случайным заполнением каждого пикселя:

var  
  c: PFColorA;
  x,y: integer;
  sprite: TFastDIB;
begin
  ...
  // создание спрайта
  sprite := TFastDIB.Create;
  // изменение размера и режима спрайта
  sprite.SetSize(64,64,32);
  // очистка черным цветом
  sprite.Clear(tfBlack);
  // находим все биты спрайта
  c := Pointer(sprite.Bits);
  // сканирование каждого пикселя
  for y := 0 to sprite.AbsHeight-1 do
  begin
  for x := 0 to sprite.Width-1 do
  begin
  // задаем случайную прозрачность пикселя
  c.a := random(256);
  // задаем случайный цвет
  c.b := (random(256) * c.a) div 255;
  c.g := (random(256) * c.a) div 255;
  c.r := (random(256) * c.a) div 255;
  // переключаемся на следующий пиксель
  Inc(c);
  end;
  // переходим на следующую линию
  c := Ptr(Integer(c)+sprite.Gap);
  end;
  // отрисовка на форму
  sprite.AlphaDraw(Form1.Canvas.Handle,0,0,255,true);
  // уничтожение объекта
  sprite.Destroy;
  ...

Данный код генерирует картинку размером 64x64 пикселя с режимом 32 бита (с поддержкой альфа-канала) со случайными цветами и случайной маской прозрачности, отрисовывает результат на форме.

Примечание: если вы хотите использовать спрайт с альфа-каналом, то отрисовывать его вы сможете только функциями AlphaDraw и AlphaStretch, т.к. только они поддерживают возможность отрисовки спрайта с альфа-каналом с прозрачностью.

Давайте рассмотрим приведенный выше код поподробнее:
переменная c типа PFColorA - это указатель на переменную типа TFColorA, который является записью и состоит из 4 байт:

PFColorA =^TFColorA;
  TFColorA = packed record
  case Integer of
  0: (i: DWord);
  1: (c: TFColor);
  2: (hi,lo: Word);
  3: (b,g,r,a: Byte);
  end;
Т.е. это указатель на данный пиксель нашего спрайта.
c := Pointer(sprite.Bits); - здесь мы указываем ссылку на все биты нашего спрайта.
Inc( c ); - здесь мы смещаемся на один пиксель дальше.
c := Ptr(Integer( c )+sprite.Gap); - здесь происходит перемещение на новую линию спрайта.
На самом деле вся картинка нашего спрайта это одна сплошная линия из пикселей, а именно одномерный массив. Т.е. в нашем случае это 64x64=4096 пикселей, а точнее 4096*4=16384 байта (Gap = 0). А переключение на следующий пиксель происходит за счет смещения адреса указателя c (Int( c )) в нашем одномерном массиве Bits. А так как мы изначально задали 32-х битный режим, то программа при смещении указателя смещает его на 4 байта автоматически.

Система игрового движка.
Теперь мы рассмотрим как же все-таки делаются все игры, и как нам начать делать свой собственный рабочий движок.

Раньше, когда я был совсем маленький, я играл со своим братом в настольные бумажные игры с кубиком. Это было очень увлекательно и забавно, но когда мы играли, то мы сами за всем следили и регулировали все правила игры, сами двигали все фигурки, сами считали баллы. Но потом у старшего брата появилась дэнди. Приставка сама все делала за нас и во много раз быстрее. Играть в игры стало интереснее и увлекательнее, т.к. мы не думали ни о чем лишнем, а были максимально сконцентрированы на самом игровом процессе, а если быть проще - тупо пялились в ящик и ломали джостики. Потом у брата появилась сега мега драйв 2, потом сега дримкаст, а потом у каждого дома уже был ПК. Игры становились все красивее и увлекательнее. Но меня всегда мучил один и тот же вопрос - "КАК?". Как работают все игры? Сама игра казалась чем-то волшебным. Мой склад ума не позволял понять всю сущность происходящих процессов, пока я сам не стал заниматься программированием. А когда я все-таки узнал "КАК", то реальность оказалась намного проще нелепых догадок.

Как известно - программа это список команд, которые понятны компьютеру, а так как это список, то и читать его полагается сверху вниз. Компьютер делает тоже самое - считывает код нашей программы сверху вниз, строка за строкой он выполняет все наши команды. Таким образом программа - это список правил игры, а компьютер - судья, который следит за выполнением всех этих правил. Единственное отличие компьютера от человека это то, что человеку достаточно один раз прочесть весь список правил (люди с склерозом не в счет) и он уже может следить за процессом игры, а компьютер каждый раз читает код программы и так до тех пор, пока игра не закончится. Одно такое считывание кода называется циклом. Каждый цикл компьютер высчитывает всю математическую составляющую игры, проверяет все устройства (ввода, звука, графики ...), следит за всем, что происходит, и выводит готовый результат в конце текущего цикла.

Для "зацикливания" нашего игрового движка так же существует много способов, но я вам расскажу о самом простом и доступном - использование стандартного компонента TApplicationEvents (вкладка Additional на панеле компонентов). Добавьте этот компонент на форму и создайте обработчик события OnIdle. Именно в этом месте и будет происходить считывание кода до конца работы программы.

procedure TForm1.ApplicationEvents1Idle(Sender: TObject;
  var Done: Boolean);
begin
  // проверка устройств ввода, объектов, прорисовка сцены и вывод на экран
  ...
  done := false;
end;

Примечание: done - переменная, отвечающая за прекращение выполнения процедуры в следующем цикле при неактивности пользователя (например если пользователь не трогает устройства ввода - мышь и клавиатуру).

Именно здесь будет проверяться вся система нашего игрового движка. Но чтобы что-то проверять - нужно все это создать.

Графическое устройство.
Вот и настал момент поближе познакомиться с графической составляющей нашего проекта. Сейчас мы рассмотрим один из примеров системы графики.

Всю работу графической системы можно разделить на три части: создание графических устройств, их проверка и отрисовка, уничтожение графических устройств. Т.е. все это можно описать в 3-х функциях:
CreateGraphicsDevices;
CheckGraphicsDevices;
DestroyGraphicsDevices;

Рассмотрим каждую функцию поподробнее:

CreateGraphicsDevices;
Здесь происходит установка нужного режима экрана, создание главной поверхности, загрузка текстур, создание других необходимых графических устройств. Чем же является "главная поверхность"? По сути дела это тот же TFastDIB, но главная она из-за того, что все объекты, весь интерфейс, все спец-эффекты рисуются именно на ней, а она уже в конце проверки графики рисуется на поверхности формы и отображает весь результат на экране.

(здесь последует продолжение статьи, но чуть позже)

#Delphi, #FastLIB, #Object Pascal

29 сентября 2013