Как сделать 2d игру: уровни и монстры, а также скрипты, интерпретаторы, искуственный интелект и создание стратегических игр.
главная страница статьи файлы о сайте ссылки
Как сделать 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

добавляется загрузчик

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.
Всё! =)