Как
сделать 2d игру: уровни и монстры, а также скрипты, интерпретаторы,
искуственный интелект и создание стратегических игр.
|
Георгий Мошкин
tmtlib@narod.ru
К этой статье появилось дополнительное объяснение.
Файлы к статье:
WAREXE.RAR - 188 kb - реализация описанного
здесь искуственного интеллекта (EXE и исходники на Delphi).
WARCRAFT.ZIP - 25kb - только исходники
(без EXE)
WARCRAFTNEW.ZIP - 25kb - исходники
с маленьким изменением от "зависания" базы
NOSIKIFULL.ZIP - 234kb - новая версия
(EXE и исходники), теперь есть строительство новых баз.
NOSIKISRC.zip - 22kb - только исходники
(без EXE).
Чтобы архитектура была абсолютно прозрачной,
надо больше использовать всякие object-ы (TMonster=object ... end;) Из
этих object-ов делать массивы (monster:array[0..100] of TMonster), а с
элементами массивов обращаться как monster[i].draw.
type TMonster=object
x,y:single;
phi:single;
spriteindex:integer;
sound:array[0..10] of integer;
...
procedure draw;
procedure ...;
...
end;
type TSprite = object
glsprite:GLUINT;
w,h:integer;
...
draw(x,y,phi:single);
...
end;
sprite:array of TSprite;
mosnter:array of TMonster;
procedure TMosnter.draw;
begin
sprite[spriteindex].draw(x,y,phi);
end;
procedure TSprite.draw(x,y,phi);
begin
gltranslatef(...);
glrotatef(...);
gltranslatef(...);
glBindTexture(GL_TEXTURE_2D, glsprite);
glColor4f(...);
glBegin(GL_QUAD);
...
glEnd;
end;
|
Тогда при написании игры уже добавлять новые процедуры, функции, типы и
переменные.
А про массивы я говорю то, что нужно будет
описать их заранее, тоже как объекты.
T2dGridElement = object
spriteindex: integer; // "tile"
flags:array[0..3] of boolean;
...
end;
T2dGridLevel = object
data:pointer;
...
setSize(w,h);
...
end;
|
Всё пишешь в отдельных файлах: mosnters.pas,
level.pas, collision.pas ...
Потом, получается, что при написании игры на этом движке пишется "движок
игры".
Про массивы: Например, проверка столкновений или "физика" где-то же должна
работать.
ЧТО давать на вход этому коллижну или "физике"? Название игры? Нет! Ты
будешь давать ему массив своего мира и массив объектов. Алгоритм проверки
столкновений проработает эти массивы, ИЗМЕНИТ что нужно в соответствии
со столкновениями и всё.
Где предмет того, с чем работает движок.
Чем движет этот движок. Может это просто тогда библиотека для вывода графики
и проверки окружностей на столкновение? Не спорю, что даже с помощью проверки
окружностей на столкновение, можно написать и тетрис, и warcraft. Но это
нужно будет писать на этом "движке". Вот после написания игры уже появится
настоящий движок.
Таким образом, для того, чтобы "архитектура
была абсолютно прозрачна" и т.п. нужно сделать движок, в котором
есть несколько разных "типов" уровней и объектов под основные типы 2d
игр.
Почему далеко до скриптов? Очень даже не
далеко. Например, вместо всяких IF-ов в программе можно сделать простенький
обработчик "скриптов":
Покажу на примере "дверь" в игре "вид сбоку".
type tdver=object
...
openscript:string;
closescript:string;
mainscript:string;
...
end;
|
Теперь при открытии двери обрабатываешь скрипт
в openscript.
Допустим, openscript="otkr.txt".
Содержание otkr.txt:
----------------------------
makenewmonster 15,18
newmusic warning.mp3
playsound opendoor.mp3
При открытии двери обрабатываешь скрипт.
У тебя интерпретатор просто по
очереди считывает строки (эти строки могут заранее быть загружены в память
для скорости).
repeat
readln(f,s);
kommanda:=StringWordGet(s,' ',1);
paramentr:=StringWordGet(s,' ',2);
INTERPTRETIROVAT(kommanda,parametr);
until(eof(f));
|
Интерпретатор простой:
procedure INTERPRETIROVAT(kommand,param:string);
begin
case kommand of
'makenewmonster': begin
LoadMonsterToLevelAt(StringWordGet(param, ',' , 1),
StringWordGet(param, ',' , 2))
end;
'newmusic': begin
BackGroundMusic:=param;
BASS_...._PlAY... или какой-нибудь FMOD_... грузит новый музон
end;
'playsound': begin
PlaySound(param);
end;
И ТУТ ЕЩЁ КУЧА ДРУГИХ ПРИЯТНЫХ ПРОЦЕДУР:
'umenshitenergiu': with Igrok[0] do begin
health:=health-1;
end;
и т.п..
end;
|
То есть дверь имеет "события" открывается,
закрывается и основной.
openscript:string;
closescript:string;
mainscript:string;
|
При открытии запускается
makenewmonster 15,18
newmusic warning.mp3
playsound opendoor.mp3
|
Появляется новый монстр в позиции 15,18
Изменяется фоновая музыка на warning.mp3
И проигрывается звук скрипящей двери opendoor.mp3
Вот тебе и скрипт.
Можно добавить строчку umenshitenergiu:
makenewmonster 15,18
newmusic warning.mp3
playsound opendoor.mp3
umenshitenergiu
|
Переход между УРОВНЯМИ я делал тоже по такому
типу.
Например, вот так (opendoor.txt):
newmusic warning.mp3
playsound opendoor.mp3
loadlevel level14.dat
|
В блок
добавляется загрузчик
case kommand of
...
'loadlevel': begin
level.load(param);
end;
...
end;
|
Загрузится level.load('level14.dat');
ОТВЕТЫ НА ВОПРОСЫ:
а) Кое-что делал.
б) А что же, всю игру запихивать в исходник? =) У меня было так: EXE я
уже не менял. Был только редактор уровней. И описанные выше "скрипты".
Всё. EXE вначале мог грузить level0.dat. Дальше дело интерпретатора.
в) Как раз наоборот. Если писать игру на "IF"-ах и всё в исходнике, не
делать редактор уровней и т.д. то проект быстро накроется. А если хорошенечко
разделить данные и движок, наладить с ними взаимодействие, сделать хороший
редактор - то дальше игру можно быстро "клепать". В моём понимании движок
- это движок, а игра - это движок плюс данные. Ими он и "движет".
г) ...
д) Ничего подобного! Например, человек пишет игрушку "вид сбоку". Если
он уже научился "двигать" графикой по экрану, то:
- делает редактор уровня по сетке
- делает вывод уровня на экран из квадратиков
- выводит туда какой-нибудь спрайт и сначала двигает "сквозь" стены
- делает простенький collision
- делает простое хождение по замкнотуму уровню
- делает редактор положения объектов
- прописывает массив игрока с поднятыми объектами
- добавляет скрипты
После написания этой базы всё становится просто. Это и есть движок. Вот
хочешь добавить "аптечку" в игру. ДА ЗАПРОСТО! Делается объект, у которого
спрайт - аптечка, а вскрипте написано. Причём это универсальный объект.
На событие ispolzovat у этого объекта есть скрипт: ispolzovat:string;
aptechka.txt:
------------------
addpower 5
playsound health.mp3
------------------
Всё! В интерпретаторе для комманд addpower и playsound есть соответствующие
пункты в блоке case:
...
addpower:begin
igrok[0].power:=power+inttostr(param);
end;
playsound:begin
..._PLAY_SOUND(param);
end;
...
|
Короче говоря, я хотел сказать следующее:
не нужно делать игру! Нужно делать ДВИЖОК и РЕДАКТОР УРОВНЕЙ. А потом
уже делать игру.
Разве "Если с этого начать (с написания
такого движка), то с 90 процентной вероятностью врятли закончишь игру."
- не бред? С таким подходом (написание игры без движка) быстро можно написать
только игру "УГАДАЙ ЧИСЛО" и т.п..
По сути можно сделать один универсальный
тип объекта, у которого есть несколько событий:
событие сразу после загрузки
событие при нахождении игрока в области объекта (хотя можно и не только
игрока!)
событие при поднятии объекта
и т.д.
У объекта будет флажок boolean типа МОЖНОПОДНЯТЬ:=true/false
Если obyekt[i].mozhnopodnyat, то мы убираем
его из массива obyekt[i] и
переносим к игроку, который поднял: igrok[j].karman[k]:=obyekt[i];
Хоть стратегия, хоть леталка - всё просто!
Всё абсолютно просто. AI - проще простого.
Делаешь две "мысли" у объекта:
glavnayaMisl:string;
vtorostepennayaMisl:string;
Делаешь интерпретатор мыслей (как и скриптов
вверху);
Например, крестьянин добывает золото, как
в WarCraft-е. Тогда главная мысль
будет чередоваться с glavnayaMisl:="goto 7";
на glavnayaMisl:="goto 441";
(В том смысле, что 7 - это индекс объекта золотника, а 441 - главная база).
Как чередоваться будут эти две мысли у AI?
А очень просто! У базы будет главный скрипт,
в котором будет условие:
bazaskript.txt
------------------
getzoloto 10
- этот скрипт будет каждый кадр требовать
10 крестьян на добычу золота
Будет идти проверка массива с ИГРОКАМИ (крестьяне,
катапульты и прочие монстры)
А КАКИЕ У НИХ МЫСЛИ. То есть будет идти подсчёт мыслей про zoloto. Допустим,
нашли
в массиве мыслей такое
...
dobichazolota 7
dostavkazolota 441
dostavkazolota 441
dostavkazolota 441
dostavkazolota 441
dobichazolota 7
dobichazolota 7
dostavkazolota 441
dostavkazolota 441
goto 441
attack 14
attack 763
attack 22
idle
idle
idle
idtiza 15
dostavkazolota 441
idtiza 15
...
|
Тут всего хватает. Двое идут к золотнику и восемь - к базе с золотом.
У крестьянина или у базы будет скрипт на событие КАСАНИЯ базы и КАСАНИЯ
золотника.
В этих скриптах будут комманды типа "если есть золото, то у базы
object[i].zoloto:=object[i].zoloto+igrok[j].zoloto,
|
а у крестьянина после этого
object[i].zoloto:=0;
igrok[j].glavnayamisl:=... //меняем на доставку
|
при касании золотника вычитаем у объекта
7 золото, прибавляем
этот кусочек золота к крестьянину и меняем его мысль на
igrok[j].glavnayamisl:=... // меняем на добычу
Если это не AI - все действия назначаются
вручную.
ВЫШЕ БЫЛО СДЕЛАНО
glavnayaMisl:string;
vtorostepennayaMisl:string;
|
Второстепенная мысль может назначатся автоматически.
Например в цикле идёт проверка всех игроков разных расс.
Если в радиусе 100 пикселей, например, есть игрок другой
рассы (igrok[i].color<>igrok[j].color), тогда врубаем
igrok[i].vtorostepennayaMisl:="goto "+inttostr(j);
|
ЭТО ЧТОБЫ ПОДОЙТИ, а потом, если произошло
касание, то
igrok[i].vtorostepennayaMisl:="attack "+inttostr(j);
|
Всё! Интерпретатор мыслей по комманде attack
будет итерационно
по кадрам задействовать алгоритм атаки. Мысли можно менять по различным
правилам.
Вы можете сделать крестьянину мысль
glavnayaMisl:="sdelatnovuyubazu". Но до этого
нужно сделать мысль "naytizolotnik".
И тут всё понятно, будет по CASE срабатывать
алгоритм блуждания по карте
КАК только в нужном радиусе окажется золотник (без препятствий по радиусу),
то
врубается мысль "sdelatnovuyubazu". Это для AI.
А человек в основном сам всё делает, но чтобы
при атаке не стояло всё на месте,
должен врубаться AI.
И так для любой игры. Игра "вид сбоку" -
ещё проще. Прыгалку типа mario сделать
вообще просто. Кто сделал это всё это в 2d - никто не мешает визуализировать
в 3d.
Всё! =)
|