Как
сделать игру - мои размышления на тему написания игры. |
Георгий Мошкин
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 самостоятельно.
Тут
написано немного про то, как делался/делается/будет делаться 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".
Есть
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.
Как
ещё я делал: грузил уровень 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
если попали, то делаем то, что заложено в объект. Если это дверь,
то открываем ИЛИ проверяем наличие ключа к этой двери в объектах
главного героя и открываем. Это может быть поднимаемый/неподнимаемый
объект. Дверь нельзя взять. Ключ можно.
2)
меняем 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
Был использован как раз тот простой редактор уровней + расстановка камер
с видом от третьего вручную (летая по уровню камерой).
|