Как сделать игру - мои размышления на тему написания игры.
главная страница статьи файлы о сайте ссылки
Как сделать игру - мои размышления на тему написания игры.

Георгий Мошкин
tmtlib@narod.ru

ОСНОВНОЕ ЧИТАЙТЕ ЗДЕСЬ

Ниже приведена кое-какая информация по разработке игры. Здесь много устаревшей информации, не исключено наличие бредовых идей. Кое-что о проекте есть на tmtlib.narod.ru.

Если просто хочется посмотреть что-нибудь интересненькое, то загляните сюда, на demoscene.ru. Ещё одна полезнейшая ссылка http://lost.biker.ru/ - фотографии мрачных мест, недостроенных зданий, заброшенных объектов.

Короче, что здесь есть:
smdview*.zip, samp67.rar - старые версии моих загрузчиков моделей в формате SMD для Delphi.
3d-tmt.zip, polyx2005.zip - новые версии загрузчиков моделей в формате SMD для TMT Pascal.
sdrm-a.zip, sdrm-b.zip - старые демки движка (только exe файлы).
Скриншоты работающего движка и некоторые идеи по созданию движка, возможно с ошибками.

Чтобы не качать огромный MilkShape3D для декомпиляции MDL моделей Half-Life существует утилита MDL DECOMPILER (138kb): mdldec12.zip. Кое-какие модели для своих экспериментов вы можете достать здесь: http://www.dhlp.de/pmodels.php3.

Если вы будете использовать загрузчик SMD в Delphi, то не забудьте насчёт размерности и цветности текстур (сделайте такие, которые держит юнит по загрузке OpenGL текстур). Дополнительные функции, позволяющие прикреплять оружие и смешивать анимации пока есть только в версии для TMT Pascal.

Загрузка Half-Life SMD в Delphi.
Ещё в далёком 2002 году мною была сделана загрузка 3d моделей из игры Half-Life, Counter Strike в Delphi с рендерингом через OpenGL. Благодаря Joachim de Vries (http://www.joachimdevries.de/) на свет появилась версия с использованием DirectX. Моя библиотека для загрузки скелетной анимции с примером использования была размещена на сайте http://delphigfx.mastak.ru. Сегодня 2005 год, идёт работа над созданием 3D игры в стиле Resident Evil с видом от третьего лица.

анимированный скелетЗагрузка Half-Life SMD в Delphi:
smdview1.zip - OpenGL версия 124kb (исходники + exe)
smdview2.zip - DirectX версия 597kb (исходники + exe)
samp67.rar - OpenGL и DirectX версия 79kb (только исходники)

Данные примеры написаны достаточно давно. Скриншот слева. К этому времени создана более продвинутая версия, но она написана на TMT Pascal 4.01 win32 target (http://www.tmt.com).

Чтобы использовать эту версию в Delphi исходники требуют больших изменений, так как я использовал особый синтаксис работы с указателями из TMT Pascal.

Так как TMT Pascal 4.01 является платной программой, и мало у кого она имеется, я сделал DOS версию для TMT Pascal 3.90, который можно скачать бесплатно (для некоммерческого использования). Так как я не планирую в ближайшее время создавать Delphi версию _actor.pas из 3d-tmt.zip, то вы можете использовать smdview1.zip, или переделать 3d-tmt.zip в Delphi самостоятельно.

скриншот из sdrm-aТут написано немного про то, как делался/делается/будет делаться SYNDROME и немного о том, как пользоваться _actor.pas (смотрите пример _example.pas) из 3d-tmt.zip (исходники под DOS) с рендерингом через polyx2005.zip (исходники под DOS) при компиляции в бесплатной версии TMT Pascal 3.90 http://pascal.sources.ru/tmt/download.htm под DOS. Справа - скриншот из демки sdrm-a.

Чуть ниже очень кратко говорится о том, как был сделаны sdrm-a.zip и sdrm-b.zip - демки под Windows (_actor.pas + OpenGL. только exe файлы без исходников). Может кому пригодится. 3d-tmt и polyx2005 может работать и в Win32 target под tmt 4.01, но нужно увеличить EXEMAX в опциях компилятора.

Полезные ресурсы.
При создании игры во многом помагают такие ресурсы, как gamedev.ru, turbo.gamedev.net, delphigamer.com, delphi3d.net, sulaco.co.za, delphigl.de и многие другие. При создании частенько приходится обращаться к исходникам на C++, и портировать их в Delphi, Pascal.

Разумеется не стоит забывать и сайт-вдохновитель re.gamefan.ru и, в особенности, их форум фанатов Resident Evil и Silent Hill!

DOS-версия: Откуда брать SMD файлы и что вообще делать дальше:
1) Берём модель в формате игры Half-Life (*.mdl).
Например, monster.mdl.

2) Декомпилируем модель в текстовые файлы с помощью
программы MilkShape 3D (http://www.swissquake.ch/chumbalum-soft/)
В меню MilkShape 3D -> TOOLS -> Half-Life -> Decompile normal HL MDL file
и выбираем monster.mdl. Или воспользуйтесь отдельной программой mdldec12.zip (138kb)

3) В результате в директории появится куча SMD
файлов типа run.smd, walk.smd, jump.smd, swim.smd и т.д. (анимация бега, ходьбы, прыжка, плавания,...). Среди этих файлов с анимацией есть файл с самой моделью. Обычно какой-нибудь refmonster.smd. В этой же директории будут текстуры модели *.BMP.

4) Теперь берём smdconv.pas из 3d-tmt.zip, компилируем и конвертим smd в pld:
smdconv.exe r refmonster.smd refmonster.pld
smdconv.exe s run.smd run.pld
smdconv.exe s walk.smd walk.pld
smdconv.exe s jump.smd jump.pld
smdconv.exe s swim.smd swim.pld и т.д.
Таким образом, саму модель со скелетом конвертим с ключом r, а анимации скелетов с ключом s.

5) Конвертим *.BMP текстуры в 24битные, и делаем их resize в каком-нибудь графическом редакторе. То есть была, например, текстура face.bmp 8бит 117x342. Её переделываем в face.bmp 24бит 128x128.

6) Конвертим *.BMP в *.TXR с помощью texture.pas из polyx2005.zip. Делается это так: компилим texture.pas получаем texture.exe
Запускаем texture.exe и вводим данные
Enter BMP: face.bmp - здесь пишем имя текстуры BMP
Enter Mode: 1 - здесь пишем цифру 1
Enter TXR: face.txr - здесь новое имя в новом формате TXR
Enter X size: 7 - здесь пишем семь, так как 2 в степени 7 = 128
Enter Y size: 7
Exter Colorkey: 32 - здесь пишем 32

7) Открываем refmonster.pld в редакторе (например FAR) и делаем замену (replace)
текста .BMP на .TXR.

8) Загружаем 3D модели (refmonster.pld) и анимацию (run.pld, walk.pld, jump.pld).

Модели можно делать в самом редакторе MilkShape 3D, а потом экспортировать в SMD.

Декомпилировать *.mdl файлы можно не только с помощью MilkShape 3D. Существуют и другие программы. Поищите в интернете "декомпилятор mdl" или "decompile mdl".

скриншот из sdrm-bЕсть exe-шные демки без исходников того, что я делал раньше. Используется всё тот же _actor.pas, но вместо POLYX+DOS отрисовка идёт через OPENGL+WIN32:
sdrm-a.zip
sdrm-b.zip
(там можно только ходить + кнопки z x c v).

В sdrm-a можно стрелять (кнопки управления аналогичны тем, что используются в Resident Evil 1 по умолчанию). Слева вы видите скриншот из sdrm-b.

 

Насчёт написания игры.
Ну, например, как сделаны демки sdrm-a и sdrm-b. Это конечно не супер, но смысл такой:

1) Есть 2D массив level:array[0..10,0..10] of myelement. - Это уровень.

2) myelement - это запись, в которой кусочек уровня
myelement = record
пол:boolean
потолок:boolean
стена1:boolean
стена2:boolean
стена3:boolean
стена4:boolean
текстура пола:integer
текстура потолка:integer
и т.д.
end;

3) Пишется процедура для вывода myelement
if стена1=true then вывод стены

4) Пишется в цикле вывод всех myelement

5) Пишется редактор: ходим курсорными клавишами по level[i,j]
i, j - текущее положение.
Задаём клавишами W,A,D,X - стена1:=true, ... стена4:=true
Стирание стен в текущем myelement: F,T,H,B - стена1:=false,...
То же самое с полом и потолком

6) Ходим курсорными клавишами по 2D массиву и расставляем стены
Делаем процедуру для записи массива level в файл (и для чтения тоже)

7) Делаем процедуру, которая проверяет x,y главного героя.
Делается это так: у нас есть положение героя x,y
также есть размер пола в одном элементе (пусть 20 на 20)
Тогда чтобы по x,y:single определить положение в уровне i,j:integer
подсчитываем: i:=round((x/20)+20/2)
j:=round((x/20)+20/2)

8) Чтобы герой мог ходить только в тех местах, где level[i,j].пол=true нужно проверять if level[i,j].пол=true

9) Ходьбу героя (изменение его x,y) делается так:
клавишами влево/вправо изменяете угол phi:single
задаёте скорость перемещения speed
раскладываете проекции speed на x,y в зависимости от phi
При нажатии вперёд делаете x:=x+(проекция speed на x)
y:=y+(проекция speed на y)

и т.д.

Т.е. смысл такой, что если вы можете сделать 2D ходилку по лабиринту, то её можно отрендерить и в 3d.

radiosity renderingКак ещё я делал: грузил уровень QUAKE (была какая-то библиотека). Но не со всеми данными, а только 3D модель уровня с текстурами. Задавал X,Y,Z главного героя где-нибудь в этом уровне. Вместо BOUNDING BOXES (которые есть в Half-Life *.mdl) делал вокруг модели несколько сфер: сфера в ногах, свера спереди и т.д. Смысл был такой, что у меня была процедура проверки пересечения сферы с треугольниками.
Есть уровень QUAKE: что-то вроде level:array of triangle; - куча треугольников. Есть положение героя x,y,z и несколько сфер радиуса R с центрами в точках x+a,y,z; x-a,y,z; x,y,z+10 радиус побольше. Потом задавал переменную dz (вертикальное направление). dz:=-0.1


Смысл такой: модель постоянно "падала" из-за dz. Но срабатывал алгоритм проверки пересечения сферы, находящейся в ногах с треугольниками из пола. И модеаль "выталкивалась" из пола до прорисовки (коррекция координатыz) Когда герой подходил к лестнице, то сначала происходило перемещение по X,Y а уже ПОТОМ проверка пересечения с треугольниками из уровня. Потом находилось такое Z, чтобы выкинуть сферу из пола. После коррекции Z отрисовывалась сцена. Т.е. сначала сфера, находящаяся в ногах, наезжала на ступеньку, но выталкивалась по Z. Чтобы не проходить сквозь стены были ещё сферы на уровне живота. Поэтому если ступенька была слишком высокая, то в неё уже не залезть.

Стоит отметить, что грузить уровни в форматах других игр мне не понравилось, поэтому сейчас я снова вернулся к идее собственного редактора уровней.

Всех герои игры в массиве:
players:array of player

player=record
x,y,z:single - положение
power:single - энергия
speed:single - возможная скорость перемещения
model:3dmolel - модель
anim:array of anim - анимации модели
тут ещё что-нибудь "текущее" для AI
end

Одним из players (например players[0]) управляем с клавиатуры (меняем x,y,z)

Для стрельбы ещё один массив
bullets:array of bullet

bullet=record
x,y,z:single
dx,dy,dz:single - куда летит
power:single - сколько отнимает при попадании
active:boolean - существует ли эта пуля в данный момент (летит=true)
speed:single - скорость пули
lifetime:single - время жизни
end

Когда мы стреляем (или AI), то находим первую "свободную" пулю в массиве bullets:
for i:=0 to length(bullets)-1 do
if bullets[i].active=false then
begin
bullets[i].active=true
bullets[i].x:=player[0].x+5*cos(phi)
bullets[i].y:=player[0].y+5*sin(phi)
bullets[i].z:=player[0].z+100 (прибавляем там уровень где оружие)

задаём скорость speed:=0.1;

задаём этой пуле направление:
bullets[i].dx:=speed*cos(phi)
bullets[i].dy:=speed*sin(phi) (куда смотрит игрок (phi), туда и стреляем)
bullets[i].dz:=0 - если пуля летит горизонтально, то z не меняется.
end;

Потом в главном цикле пуля летит и делается
x:=x+dx, y:=y+dy и т.д.
Если в кого-то попала (или в нас самих), то вычитаем из power игрока
power пули. И делаем пулю неактивной: bullets[i].active:=false

Искуственный интелект можно сделать на уровне зомби (не сложно). Действия по таймеру, выбор поворота phi при столкновениях влево/вправо по random=0 или random=1. Хотя не исключено применение более совершенных алгоритмов.

Т.о. должны быть массивы с игроками, массивы с пулями. Все элементы должны содержать записи - положение, всякие параметры. Массив с уровнем может быть просто из треугольников (полигонов), а к нему - довесок из массивов объектов.
Например:
level:array[0..100000] of triangle

где triangle=record
x:array[0..2] of single;
y:array[0..2] of single;
z:array[0..2] of single;
texture:integer;
end

Довесок:
levelobjects:array[0..50] of lobj

где lobj=record
objtype:integer - тип объекта
active:boolean - есть он или нет (его могли уже взять или он уничтожен, или
вообще не появился пока по сценарию)
x,y,z:single - положение в уровне
radius:single - радиус объекта
end;

Дальше, помимо всех этих данных должны быть циклы с проверками.
Например, по нажатию клавиши Space, проверять
player[0].x
player[0].y
player[0].z
главного героя на нахождение внутри сферы радиуса levelobjects[i].radius с центром в
levelobjects[i].x
levelobjects[i].y
levelobjects[i].z
Если объект можно взять, то заносим его в массив объектов, которые есть у
player[0] и удаляем его из levelobjects[i] (делаем levelobjects[i].active:=false).
Если положить нужно i-тый обьект, то делаем в player[0].objects[i]:=false, а в уровне levelobjects[i]:=true и levelobjects[i].x:=player[0].x
levelobjects[i].y:=player[0].y, levelobjects[i].z:=player[0].z (положили в ноги игрока).

Также должны быть циклы с отрисовкой обьектов:
треугольники (полигоны) уровня
объекты уровля levelobjects[i] (если они active=true)
игроки player[j]
летящие пули (или стрелы) bullets[k] (если они active=true)

Таким образом, будет цикл игры:
repeat
1) нажаты клавиши

1.1) вверх вниз - меняем (x,y,z) у player[0]
x:=x+speed*cos(phi);y:=y+speed*sin(phi)

1.2) влево вправо - меняем (phi) у player[0]
if key=vk_left then phi:=phi-1 и т.п.

1.3) пробел - проверяем (x,y,z) на попадание в сферы из levelobjects
если попали, то делаем то, что заложено в объект. Если это дверь,
то открываем ИЛИ проверяем наличие ключа к этой двери в объектах
главного героя и открываем. Это может быть поднимаемый/неподнимаемый
объект. Дверь нельзя взять. Ключ можно.

radiosity, вывод через opengl2) меняем x,y,z у player[1], player[2], player[3], ... по алгоритму
если это зомби, то они могут просто пытаться идти в направлении
игрока (посчитать вектор в направлении игрока, отнормировать его
и задать направление движение зомби по этому вектору. Если
зомби попадёт в стену, то отключить стремление к главному герою
по boolean на определённое время и сделать обход стены по random-у

3) проверяем пересечение игроков и объектов с уровнем. Объекты и игроки
постоянно "падают" g=9.8 Корректируем координаты, если нужно.

4) проверяем пересечение bullets[i] с игроками player[j] (а может даже и
с объектами levelobjects[k] - разрушаемые пулями объекты, которые
обладают levelobjects[k].power и разрушаются levelobjects[k].active:=false
при достижении levelobjects[k].power=0)

5) устанавливаем камеру в нужное положение (вид от третьего лица, от первого
лица и т.д.)

6) отрисовываем все player[i], levelobjects[k], и level[l].polygons

7) если мы попали на levelobjects[k], который изменяет уровень, то
загружаем другой уровень и расстанавливаем там по местам
player[i] и levelobjects[k]. В объекте-"телепортаторе" (может выглядеть
как обычная дверь, или машина с открытой дверью) есть ссылка на
имя файла другого уровня и где мы там должны появиться (x,y,z)
until exitgame=true

Применение загрузчика SMD в разных графических движках (OpenGL, POLYX, DirectX,...).
Манипуляций конечно многовато, но всё можно существенно упростить:
Написать свою (или приспособить уже имеющуюся) библиотеку для загрузки текстур сразу из BMP. Тогда останется только преобразовывать MDL->SMD->PLD (это быстро). Или вообще, если делать самому 3D модели в MilkShape3D, то просто всегда их записывать в формате SMD. В компиляторах с win32 target с текстурами проще: можно использовать OpenGL + юнит для загрузки текстур.

Можете вообще выбросить на время загрузку текстур и прикрутить любой свой 3d движок, в котором можно рендерить полигоны-треугольники:

выбросте загрузку Textures^[i].Load
там где __with locvert^[j].tcrd do__
__with locvert^[j].nr do__ и __with locvert^[j].crd do__
вставте всё для своего движка. Если это OpenGL, то эти три with заменятся на

[CODE=pas]
for j:=0 to 2 do begin

glTexCoord2f((triangle+sizeOf(triangle^)*i)^.coord[j].u,
(triangle+sizeOf(triangle^)*i)^.coord[j].v);

glNormal3f((vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.norm[0],
(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.norm[1],
(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.norm[2]);

glVertex3f((vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.coord[0],
(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.coord[1],
(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.coord[2]);

end;
[/CODE]

в случае с дивжком POLYX это выглядит так:

[CODE=pas]
for j:=0 to 2 do begin

with locvert^[j].tcrd do
begin
u:=round( 1023 * (triangle+sizeOf(triangle^)*i)^.coord[j].u );
v:=round( 1023 * (triangle+sizeOf(triangle^)*i)^.coord[j].v );
end;


with locvert^[j].nr do
begin
x:=(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.norm[0];
y:=(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.norm[1];
z:=(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.norm[2];
end;

with locvert^[j].crd do
begin
x:=(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.coord[0]*100;
y:=(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.coord[1]*100;
z:=(vertex+sizeOf(vertex^)*( (triangle+sizeOf(triangle^)*i)^.vertexId[j]))^.coord[2]*100;
end;

end;
[/CODE]

Поэтому к _ACTOR.PAS из 3d-tmt.zip можно прикрутить любой рендеринг, где можно отрендерить оттекстурированный треугольник
x0,y0,z0,nx0,ny0,nz0,u0,v0
x1,y1,z1,nx1,ny1,nz1,u1,v1
x2,y2,z2,nx2,ny2,nz2,u2,v2
т.е. вершины,нормали,текстурные координаты

И текстуры грузить хоть PNG,JPG,BMP,GIF - любые, которые держит ваш движок для отрисовки текстурированных треугольников.

Зачем нужна скелетная анимация.
Кому вообще это может пригодиться: если нужна скелетная анимация (как в моделях Half-Life). Смысл здесь какой: можно прямо в игре крутить головой персонажа, его туловищем (менять rotation у нужного node). Прикреплять оружие или любой другой 3D объект (объект создаётся smdconv.exe с ключом o) с помощью .attach(актёр,node).

Допустим:

загрузили оружие в gun:TObject;
загрузили молель в player:TActor;
загрузили ходьбу в walk:TAnimation;

Делается это так:

gun.upload('gun.pld');
player.upload('monster.pld);
walk.upload('walk.pld');

потом установили FPS анимации walk.FPS:=30
и отключили движение нулевого node walk.FIX:=true

Дальше, в главном цикле игры
player.animate(walk,1)
player.render
elapsed:=0.1

если сделать
player.animate(walk,-1), то будет задом наперёд

чтобы была одинаковая скорость анимации на разных компьютерах elapsed приравнивать не числу 0.1, а настоящему времени отрисовки кадра.

Если в руке должно находиться оружие "gun.pld", то делаем так:
Допустим, сустав у кисти имеет номер 15, тогда чтобы прикрепить туда пушку
делаем:
gun.attach(player,15)
gun.render

Если хотим крутить головой персонажа (чтобы он куда-нибудь посмотрел), то нужно изменить углы вращения в этом месте скелета: пусть голова висит на node под номером 33. Пусть туловище висит на node 7. Тогда если покрутим node 7, то будет крутиться всё, вместе с головой и руками (потому что это скелет). Как конкретно это я делал, я немного подзабыл: но нужно просто до player.animate покрутить чем надо.

Можно накладывать анимации друг на друга, если
player.animateBlend(walk,walkbad,p);
p:= процент того, сколько брать от walk, а сколько от die
p:=от 0 до 1.

То есть walk - это ходьба в здоровом состоянии, а
walkbad - в больном (прихрамывание и т.п.)

Не раненый: player.animateBlend(walk,walkbad,0);
Немного раненый: player.animateBlend(walk,walkbad,0.25);
Уже совсем никакой, хромает и волочит ногу: player.animateBlend(walk,walkbad,1);

В принципе подойдут скелетно анимированные модели и из других игр (загружать через MilkShape3D и записывать в SMD).

Игру сделать можно, только сейчас меня больше интересует качество картинки: делаю освещение по Radiosity (скриншоты в тексте выше). Новый редактор уровней (выше был описан совсем простой). Раньше в плане создания игры было сделано вот это:
sdrm-a.zip
sdrm-b.zip
Был использован как раз тот простой редактор уровней + расстановка камер с видом от третьего вручную (летая по уровню камерой).