Создание движка для игры "Варяг" - часть 5.
главная страница статьи файлы о сайте ссылки
Создание движка для игры "Варяг" - часть 5.

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

Часть 1 - Часть 2 - Часть 3 - Часть 4 - Часть 5 - Часть 6 - Часть 7

Перед тем, как мы начнём двигать игрока, нам необходимо выбрать его мышью. Но мы сразу пойдём по пути унификации, и обеспечим возможность выбора не только одного юнита, но и группы юнитов.

Как вы помните, в играх типа WarCraft существует выбрать сразу нескольких персонажей. Например, пять воинов и одного крестьянина. И всех их куда-нибудь послать. В принципе, это неплохая идея: найти новый золотник, крестьянин будет строить базу, воины охранять неокрепшую базу. Как только база готова - строим вышки, стреляющие стрелами, покупаем новых крекстьян и развитие идёт полным ходом!

Я предлагаю такой подход, при котором уже сейчас мы одновременно воссоздадим искуственный интеллект. Основную идею создания искуственного интеллекта я изложил в статье "Как сделать 2d игру: уровни и монстры, а также скрипты, интерпретаторы, искуственный интелект и создание стратегических игр". Я сделаю всё таким образом, что попросту объединю код искуственного интеллекта с кодом передвижения игрока. Смотрите: вместо того, чтобы плодить лишние переменные типа "пункт назначения" и т.п., мы будем пользоваться интерпретатором мыслей. Причём если рассой управляет компьютер, то он и задаёт мысли. А вот если рассой управляет человек, то мысли будут задаваться при нажатии на мышку или на какие-нибудь элементы управления. Но это не означает, что расса, которой управляют с помощью мышки или клавиатуры, будет полностью лишена искуственного интеллекта: например, если к воину подойдёт какой-нибудь вражеский громила и начнёт его колотить, то мы в любом случае зададим мысль "дать сдачи", чтобы наш персонаж не стоял на месте, а автоматичски начинал сопротивляться. Подобное логичное поведение вы без труда найдёте в играх типа WarCraft.

Сначала я прокомментирую приготовленый мною исходный код dev001.rar (85kb). Процедуру загрузчика ZagruzOpisan(fname:string;var fRassa:TRassa) описаний юнитов я дополнил двумя обработчиками:

if razdel='индексы' then
begin
...
end;

и

if razdel='параметры' then
begin
...
end;

В файле описания крестьянина есть соответствующие разделы:

)название
крестьянин крестьянин

)спрайты
.\ludi\peasant.bmp
.\ludi\peasant with gold.bmp

)индексы
0 1 2 3 2 1 4 5 4
0 6 7 8 9 10
0 11 12 13
1 1 2 3 2 1 4 5 4

)параметры
36 512 1024 72 72

...

Вы без труда сможете разобраться в содержиомом блоков IF, предназначенных для парсинга данных текстовых строк. В блоке парсинга индексов присутствуют следующий код:

...
fRassa.Opisan[i].posledovatelnost[j].texturnum:=StrToInt(StringWordGet(s, ' ',1));

for n:=2 to 2+m-1 do
fRassa.Opisan[i].posledovatelnost[j].kadr[n-2]:=StrToInt(StringWordGet(s, ' ',n));
...

Рассмотрим как это работает на примере чтения строки индексов "0 1 2 3 2 1 4 5 4". Формат хранения индексов следующий: сначала идёт номер текстуры, а затем несколько номеров строк. В данном случае номер текстуры нулевой. Число ноль запишется в texturnum: сначала функция StringWordGet() выдаст первое слово из строки, а затем мы преобразуем данный текст в число с помощью функции StrToInt. Происходит следующе:

texturnum:=StrToInt(StringWordGet('0 1 2 3 2 1 4 5 4', ' ', 1));

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

for n:=2 to 2+m-1 do ...

Как раз и получается, что сначала мы первое слово (0) записали в texturnum, а все последующие слова (1 2 3 2 1 4 5 4) мы в цикле записываем в kadr[0], kadr[1], kadr[2] и т.д..

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

if razdel='параметры' then
begin
fRassa.Opisan[i].radius:=StrToInt(StringWordGet(s, ' ',1));
fRassa.Opisan[i].XSize:=StrToInt(StringWordGet(s, ' ',2));
fRassa.Opisan[i].YSize:=StrToInt(StringWordGet(s, ' ',3));
fRassa.Opisan[i].sprX:=StrToInt(StringWordGet(s, ' ',4));
fRassa.Opisan[i].sprY:=StrToInt(StringWordGet(s, ' ',5));
end;

Здесь RADIUS - это радиус, описанный вокруг юнита. Данный параметр предназначен для проверки столкновений. XSize, YSize - это размеры текстуры. sprX, sprY - это размеры спрайта.

Теперь я покажу, как процедура StringWordGet будет учавствовать в чтении данного раздела:

)параметры
36 512 1024 72 72

Это всё равно, что:

if razdel='параметры' then
begin
fRassa.Opisan[i].radius:=StrToInt('36');
fRassa.Opisan[i].XSize:=StrToInt('512');
fRassa.Opisan[i].YSize:=StrToInt('1024');
fRassa.Opisan[i].sprX:=StrToInt('72');
fRassa.Opisan[i].sprY:=StrToInt('72');
end;

Всё очень просто. С помощью процедуры StringWordGet мы взяли нужные слова из строки. Словами в данном случае являются цифры. Но так как мы прочитали текстовую строку, и разбили её на текстовые слова, то нам необходимо применить функцию StrToInt, которая преобразует STRING в INTEGER.

В переменных появился следующий тип:

type TKadri=record
texturnum:integer;
kadr:array of integer;
end;

Здесь texturnum - это номер текстуры, из которой следует брать спрайты, а kadr - это массив с номерами строк (как вы помните, в одной текстуре у нас несколько строк со спрайтами).

У каждого юнита есть несколько анимационных последовательностей, которые хранятся в массиве posledovatelnost:

type TOpisan=record
nazv :string;
nazvdisp :string;

spritestex :array of GLUINT;
posledovatelnost: array of TKadri; // 0 - ходьба, 1 - атака, 2 - падение, 3 - альтернативная ходьба
radius : single;
XSize,YSize:integer;
sprX, sprY : integer;


energia :array of TEnergia;
skorost :array of single;
udar :array of TUdar;
stroyka :array of string;
remont :array of string;
dobicha :array of TDobicha;
upgrade :array of TUpgrade;

cena :array of TCena;
neobh :array of TKolvo;

resurs :boolean;
resursmax :single;

end;

Для анимирования персонажа нам необходимо знать номер текущего кадра (TekushKadr) и номер последовательности (TekushPosl):

type TObyekt = record
nomRas:integer;
nomTip:integer;
nomDru:integer;
energia :array of single;
resurs :array of single;
x,y,phi :single;

TekushKadr:single;
TekushPosl:integer;

MISL1:string;
MISL2:string;
end;

Также введены две переменные MISL1 и MISL2, которые будут использоваться в интерпретаторе мыслей. Мысли мы храним в текстовом виде. Приведу пример мысли:

MISL1:='idti 150, 775'

Эта мысль говорит, что объект должен идти в точку с координатами x=150, y=775. Как только объект достигнет этой точки, мы обнулим его мысль:

MISL1:=''

Передвежение игрока обеспечится интерпретатором мыслей, о котором речь будет идти ниже. Но смысл такой: мы с помощью функции StringWordGet разделим строку "idti 150,775" на строку "idti" и строку "150,775". Дальше в интерпретаторе будет присутствовать блок IF:

if MISL1='idti' then
begin
...
end;

Внутри begin-end мы превратим строку "150,775" в числа x=150, y=150 и будем двигать игрока. Как только игрок окажется в нужной точке, мы сделаем удаление данной мысли:

if (igrokX=x) and (igrokY=y) then MISL1:=''

Поэтому при следующем проходе интерпретатор мыслей не выполнит блок if MISL1='idti' then. Зачем нужна вторая мысль MISL2 - я расскажу позже. Но в принципе сейчас вы уже должны видеть мощь данной простой системы.

Для облегчения процесса создания новых юнитов удобно ввести какую-нибудь процедуру. Подробное описание будет дано в комментариях к главному релизу исходников. А сейчас лишь поясню смысл процедуры

procedure NewObyekt(nomRas, nomTip, nomDru:integer;x,y,phi:single);

Данная процедура в каком-то смысле и занимается клонированием типов. Всё, что она делает - это создаёт новый объект (юнит) нужной нам рассы nomRas, нужного типа nomTip, с определённым номером дружбы nomDru. Причём из описания типа берутся максимальные уровни энергий, соответствующих данному юниту, и заносятся в соответствующие записи.

Вывод данных с учётом анимации осуществляет процедура SuperKvadrat:

procedure SuperKvadrat(nomerObj,nomerPosl:integer);

Данная процедура анализирует параметры объекта под номером nomerObj и проигрывает анимационную последовательность под номером nomerPosl. Процедура SUPERkvadrat в каком-то смысле является надстройкой над написанной ранее процедурой Kvadrat.

Вывод всех юнитов на экран производит процедура RenderObjects, которая в цикле вызывает процедуру SuperKvadrat для всех юнитов уровня:

for i:=0 to length(Obyekti)-1 do
begin
m:=Obyekti[i].nomRas;
n:=Obyekti[i].nomTip;

if Obyekti[i].energia[0]>0 then
begin
SuperKvadrat(i,Obyekti[i].TekushPosl);
end;

end;

Перейдём к процессу выбора игрока. Для начала сделаем выбор хотя бы одного игрока, а не целой группы. Введём переменную Vibran:

var Vibran:integer=-1;

При нажатии правой конпки мышки мы проверям, нет ли в близлежаших точках какого-нибудь юнита. Если юнит есть, то заносим его номер в переменную Vibran.

a:=MyMouse.X-movex-Obyekti[i].x;
b:=MyMouse.Y-movey-Obyekti[i].y;
fff:=sqrt(a*a+b*b);
ppp:=Rassa[Obyekti[i].nomRas].Opisan[Obyekti[i].nomTip].radius;

if fff<ppp then
if myMouseLeft then
begin
vibran:=i;
end;

 

Выше мы находим вектор с координатами (a,b). Данный вектор проведён между точкой, в которой расположен объект (юнит) и другой точкой, в которой была нажата мышь. Координаты i-того объекта: Obyekti[i].x, Obyekti[i].y. Координаты мыши: MyMouse.X, MyMouse.Y. А переменные movex и movey - это смещение карты относительно экрана. Положение мышки в координатах карты с учётом смещения карты: (MyMouse.X-movex, MyMouse.Y-moveY). Затем подсчитали длину fff вектора (a,b). Иными словами, мы узнали расстояние fff между юнитом и курсором мышки с учётом смещения карты. В переменную ppp мы занесли радиус окружности, описанной вокруг юнита. Потом идёт проверка: если расстояние fff меньше радиуса ppp, и при этом нажата левая кнопка мыши, то выбран данный объект (номер i). На самом деле это простая проверка: находится ли курсор мыши внутри окружности радиуса Rassa[Obyekti[i].nomRas].Opisan[Obyekti[i].nomTip].radius.

Для красоты и повышения удобства игры добавляем подсветку игрока крутящимя кружком. Для этого при выполнении условия fff<ppp вызываем процедуру Kursor(0,ppp).

Итак, допустим мы выбрали игрока. Теперь мы знаем его номер (Vibran:integer). Последовательность дальнейших действий следующая:

  • В меню выбрали команду "идти"
  • Включается режим ожидания нажатия ПРАВОЙ кнопки мыши на карте
  • При нажатии ПРАВОЙ кнопки мыши на карте мы задаём игроку мысль MISL1="idti куда"

Рассмотрим соответствующий код:

if myMouseRight then
begin
Obyekti[vibran].MISL1:='idti '+intToStr(round(MyMouse.X-movex))
+','
+intToStr(round(MyMouse.Y-movey));
end;

А так как в главном цикле игры каждый раз вызывается интерпретатор мыслей, то юнит немедленно начнёт передвигаться. Исходный код процедуры интерпретатора мыслей:

procedure InterpretirovatMisli;
var misl : string;
params : string;
i:integer;

a,b:string;
x,y:single;
x1,y1:single;
fff:single;

m,n:integer;

begin

for i:=0 to length(Obyekti)-1 do
begin

m:=Obyekti[i].nomRas;
n:=Obyekti[i].nomTip;

misl:= StringWordGet(Obyekti[i].misl1,' ',1); - мысль
params:= StringWordGet(Obyekti[i].misl1,' ',2); - параметры мысли

if misl='' then Obyekti[i].TekushPosl:=-1; - если мыслей нет, то текущая анимационная последовательность = -1, что соответствует анимации "юнит стоит на месте"

if misl='idti' then
begin

Obyekti[i].TekushPosl:=0; - последовательность = 0, что здачит "идёт"

a:=StringWordGet(params,',',1);
b:=StringWordGet(params,',',2);

x:=StrToInt(a);
y:=StrToInt(b);

x:=x-Obyekti[i].x;
y:=y-Obyekti[i].y;

fff:=sqrt(x*x+y*y);

if fff<Rassa[m].Opisan[n].radius/2 then Obyekti[i].misl1:=''; - если пришли, то обнуляем мысль

if fff>0 then
begin
x:=x/fff;
y:=y/fff;
end;

x1:=1*cos(DegToRad(Obyekti[i].phi));
y1:=1*sin(DegToRad(Obyekti[i].phi));

fff:=x*x1+y*y1;

fff:=arccos(fff);

fff:=RadToDeg(fff);

if fff>5 then - если текущее направление передвижения отличается от нужного более, чем на 5 градусов, то начинаем крутить юнит вокруг своей оси (как бы режим поиска)
Obyekti[i].phi:=Obyekti[i].phi+5;


fff:=DegToRad(Obyekti[i].phi);

Obyekti[i].x:=Obyekti[i].x+0.3*cos(fff); - передвижение юнита в направлении fff
Obyekti[i].y:=Obyekti[i].y+0.3*sin(fff); - передвижение юнита в направлении fff


end;

end;

end;

А теперь вернёмся к выводу анимационных спрайтов. Как вы помните, столбцы соответствуют различным углам поворота, а строки - различным кадрам анимации. Процедура вывода спрайта:

procedure SuperKvadrat(nomerObj,nomerPosl:integer);
var m,n:integer;
var kolonka:integer;
var zerkalno:boolean;
var XSize,YSize,sprX,sprY:integer;
var kadr:integer;
var nomerPosledovatelnosti:integer;
var tempAngle:single;
begin

m:=Obyekti[nomerObj].nomRas;
n:=Obyekti[nomerObj].nomTip;

if nomerPosl<0 then
begin
Obyekti[nomerObj].TekushKadr:=0;
nomerPosledovatelnosti:=abs(nomerPosl)-1;
end
else
nomerPosledovatelnosti:=nomerPosl;


if Obyekti[nomerObj].TekushKadr>
length(Rassa[m].Opisan[n].posledovatelnost[nomerPosledovatelnosti].kadr)-1
then Obyekti[nomerObj].TekushKadr:=0;

Здесь блок, представляющий основной интерес:
zerkalno:=false;
tempAngle:=Obyekti[nomerObj].phi-90;
if tempAngle<0 then tempAngle:=360+tempAngle;
kolonka:=round( tempAngle ) div 45 + 1;
if kolonka>5 then kolonka:=5-(kolonka-5) else zerkalno:=true;
В результате данных расчётов мы получаем номер колонки (столбца) со спрайтами, а также нужно ли нам его зеркально отражать.

XSize:=Rassa[m].Opisan[n].XSize;
YSize:=Rassa[m].Opisan[n].YSize;
sprX:=Rassa[m].Opisan[n].sprX;
sprY:=Rassa[m].Opisan[n].sprY;

glBindTexture(GL_TEXTURE_2D,
Rassa[m].Opisan[n].spritesTex[Rassa[m].Opisan[n].
posledovatelnost[nomerPosledovatelnosti].texturnum]);

kadr:=round(Obyekti[nomerObj].TekushKadr);


glpushMatrix;
glTranslatef(Obyekti[nomerObj].x-sprX/2, Obyekti[nomerObj].y-sprY/2,0);

Kvadrat(XSize,YSize,
sprX,sprY,
Rassa[m].Opisan[n].posledovatelnost[nomerPosledovatelnosti].kadr[kadr],
kolonka,
zerkalno);

glpopMatrix;

Obyekti[nomerObj].TekushKadr:=Obyekti[nomerObj].TekushKadr+0.1;

end;

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

Файлы к статье: dev002src.rar - исходники (144kb), dev002exe.rar - исходники + exe (329kb).

Часть 1 - Часть 2 - Часть 3 - Часть 4 - Часть 5 - Часть 6 - Часть 7