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

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

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

Как вы помните, в прошлой части мы остановились на формате описания юнитов. Напомню вам, что для описания юнитов я решил использовать текстовые файлы. Каждому виду юнитов у нас соответствует отдельный текстовый файл. Например, это могут быть krestyanin.txt, katapulta.txt, ferma.txt, zolotnik.txt и т.д..

Для чтения данных из текстовых файлов мы объявили переменную f типа text:

var f: text;

А так как размеры различных массивов у нас имеют переменную длину, которую можно менять с помощью процедуры setlength(), то сразу же появилась возможность унифицировать все юниты. Суть состоит в следующем: допустим, в текстовом файле krestyanin.txt есть раздел "ремонт":

)ремонт
база
ферма

Это означает, что крестьянин может ремонтировать базу и ферму. Если вы уже в самой игре выберете крестьянина с помощью курсора, то в меню будет кнопка по ремонту, которую можно нажать (вспомните warcraft).

Теперь посмотрим, что же находится в разделе "ремонт" у катапульты. И что же мы видим: у катапульты (katapulta.txt) вообще нет такого раздела!

В этом и состоит унификация, позволяющая сделать огромное разнообразие юнитов без написания дополнительного исходного кода. Всё решается на этапе загрузки. Посмотрим исходный код rtsmain.pas, соответствующий анализу раздела "ремонт":

IF razdel='ремонт' then

begin

SetLength(fRassa.Opisan[i].remont, length(fRassa.Opisan[i].remont)+1);

j:=length(fRassa.Opisan[i].remont)-1;

fRassa.Opisan[i].remont[j]:=s;

end;

У катапульты (katapulta.txt) мы вообще никогда не сработает этот IF, соответствующий анализу раздела "ремонт". А это означает, что не будет запущена процедура SetLength, которую я использую для увеличения массива fRassa.Opisan[i].remont.

Когда данные будут уже загружены, то в самой игре мы всегда можем проанализировать длину массива "ремонт". Если длина нулевая - не будем выводить кнопку ремонт, или выведем её затемнённым цветом, что она недоступна для нажатия.

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

type TOpisan=record
nazv :string;
nazvdisp :string;
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;

radius :single;
end;


type TRassa = record
Opisan:array of TOpisan;
end;

var Rassa:array of TRassa;

Итак, допустим мы загрузили две рассы: люди и орки. Это означает, что массив Rassa будет состоять из двух элемнтов.

У каждой рассы есть массив юнитов Opisan:array of TOpisan. Например, пусть 0 - индекс рассы людей, 1 - индекс рассы орков. Тогда к данным 14-того вида юнита у орков можно обратиться так:

Rassa[1].Opisan[14]....

Так как мы уже упомянали катапульту и размер массива переменной длины, то полезно будет показать как всё-таки узнать размер массива "ремонт". Представим, что 8-ой вид юнита - это и есть катапульта, тогда:

if (length(Rassa[1].Opisan[8].remont) = 0) then
ShowMessage ('Не может ничего ремонтировать!');

А вот если бы 8-мой вид юнита был крестьянином, то мы бы не получили сообщения 'Не может ничего ремонтировать!'.

Казалось бы, можно было бы уже приступать вплотную к написанию искуственного интеллекта и отображения карты на экран. Это действительно так, но перед этим стоит вспомнить ещё одну деталь, о которой я упомянал в прошлой части.

Представим, что у рассы людей есть 10 типов юнитов с учётом всех будущих апгрейдов. Но во время игры у нас может быть 5 юнитов первого типа, 17 юнитов второго типа, 300 юнитов третьего типа, 3 юнита четвёртого типа и т.д.. Поэтому в целях экономии памяти и создания более логичного и понятного исходного кода целесообразно описания юнитов хранить отдельно от изменяющихся параметров юнитов. Именно поэтому в прошлой части описания юнитов было решено хранить здесь:

type TRassa = record
Opisan:array of TOpisan;
end;


var Rassa:array of TRassa;

А изменяющиеся параметры - здесь:

type TObyekt = record
tipS :string;
tipI :integer;
energia :array of single;
resurs :array of single;
x,y,phi :single;
end;

var Obyekti:array of TObyekt;

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

  • номер рассы
  • номер типа юнита
  • номер дружбы

type TObyekt = record
nomRas:integer;
nomTip:integer;
nomDru:integer;

energia :array of single;
resurs :array of single;
x,y,phi :single;
end;

var Obyekti:array of TObyekt;

Вот теперь, после этих исправлений, у нас всё на месте. Подитожим что же здесь всё-таки было сделано.

Во-первых, где хранятся юниты, которые будут бегать по экрану? Все бегающие, плавающие и неподвижные юниты, включая золотники и деревья, будут храниться в массиве var Obyekti:array of TObyekt. Причём обратите внимание, что в этом массиве хранятся абсолютно все герои игры, т.е. вперемешку разные рассы, и в довершение ко всему деревья, золотники и прочая нечисть.

Второе - как мы узнаем о способностях юнита? Всё просто. У нас есть три переменные: номер рассы, номер типа юнита и номер дружбы. Тогда для юнита 578 мы можем узнать абсолютно всю необходимую информацию для "подстановки" её в движок и вывода на экран:

берём Obyekti[578]
и видим: Rassa[Obyekti[578].nomRas].Opisan[Obyekti[578].nomTip].remont

То есть мы как бы наделали клонов. У нас были изначальные типы с описаниями. А реальные юниты представляют из себя всего лишь координаты, угол поворота, уровни энергии и ссылки на индексы базовых описаний.

А теперь о так называемом "номере дружбы". Допустим, у вас есть только две рассы: орки и люди. Но в игре вы хотите сделать вражду между орками и орками. Ну или между людьми и людьми. Так вот, если у юнитов не равен этот номер, то они являются врагами!

Приведу пример двух врагов:

Obyekti[1].nomDru:=100
Obyekti[2].nomDru:=200

Кстати, в зависимости от этого номера можно несколько видоизменять цвета спрайтов. Ведь для орков спрайты у нас одинаковые. Тогда мы просто в зависимости от номера дружбы подкорректируем цвета: у всех спрайтов, для которых номер дружбы = 100, делаем зеленоватый оттенок. А у тех, что 200 - красноватый.

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

Ну а вот теперь, когда мы более-менее вспомнили предыдущие наработки по формату файлов с описаниями юнитов, можно вплотную приступить к написанию исходников для реализации карты, реализации движения игрока и предварительному этапу создания искуственного интеллекта.

Карта у нас будет следующая: все идут куда хотят, никаких сеток и т.п.. Деревья, золотники и дома можно выстраивать в любых точках, и опять же НИКАКИХ сеток. А вот потом, когда уже всё это заработает, можно будет сделать какую-нибудь надстройку для выравнивания по сетке. Очень вероятно, чтомы вообще никогда никаких сеток делать не будем.

Сейчас же все юниты у нас будут перемещаться не между квадратами сетки, а между реальными любыми координатами. Например, между x=0.001, y=1 и x=-14.33312, y=303.5. То есть забудем о сетках. У кого-то может возникнуть вопрос: а как же воду описывать без сеток и т.д.. Ответ следующий: мы можем описывать области, и никаких сеток не понадопится. Например, треугольный пруд. Неподалёку - круглое болото. Деревья торчат в любых координатах.

На данный момент карта у нас почти бесконечная. Ограничение состоит в предельных значениях для переменных X и Y типа SINGLE. Заглянем в помощь по Delphi и видим:

1.5 x 10^–45 .. 3.4 x 10^38

Итак, с картой мы разобрались.

Теперь перейдём к реализации движении игрока и одновременно будем создавать зачатки искуственного интеллекта. Нам нужно реализовать следующее:

  • выбрали крестьянина мышкой
  • нажали какую-нибудь точку на карте
  • крестьянин пошёл в нужную точку

При этом нужно врубить у спрайта нужные кадры анимации, а также учесть угол поворота.

Первое - это загрузка нужных спрайтов для каждого типа юнита. Идентификаторы загруженных OpenGL-евских текстур имеют тип GLUINT. Введём массив sprite переменной длины, в который можно загружать текстуры (спрайты) юнитов:

type TOpisan=record
nazv :string;
sprite :array of GLUINT;
nazvdisp :string;
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;

radius :single;
end;

Добавим в файл krestyanin.txt раздел "спрайты":

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

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

)энергия
жизнь 10 600
магия 3 600

)скорость
земля 10
вода 3

)удар
кувалда жизнь 1 нет 0
супердуб жизнь 3 магия 1

)стройка
база
ферма

)ремонт
база
ферма

)добыча
золото 10 5
лес 10 30

)апгрейд

]название
воин

]ресурсы
золото 10 60
лес 20 60

]объекты
база 1

]название
супервоин

]ресурсы
золото 20 60
лес 40 60

]объекты
база 1

)цена

]ресурсы
золото 50 25
лес 10 25

]объекты
ферма 0.3

И напишем обработчик, который сработает при загрузке раздела "спрайты":

if razdel='спрайты' then
begin
SetLength(fRassa.Opisan[i].Sprite,
length(fRassa.Opisan[i].Sprite)+1);
j:=length(fRassa.Opisan[i].Sprite)-1;

LoadTexture(s,fRassa.Opisan[i].Sprite[j]);
end;

Но нам ещё нужно знать из каких строк большой текстуры со спрайтами брать спрайты ходьбы, атаки, ходьбы с золотом для крестьянина. В связи с этим в файле krestyanin.txt добавим раздел с индексами строк:

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

)спрайты
.\ludi\peasant.bmp
- это нулевая текстура со нормальными спрайтами (0)
.\ludi\peasant with gold.bmp
- это первая текстура со спрайтами, где крестьянин с золотом (1)

)индексы
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

)энергия
жизнь 10 600
магия 3 600

Итак, читаем: для ходьбы берём нулевую текстуру (peasant.bmp) и меняем кадры, соответствующие строкам 1, 2, 3, 2, 1, 4, 5, 4. Для атаки или рубки деревьев берём нулевую текстуру и последовательно прокручиваем спрайты из строк 6, 7, 8, 9 и 10. Если данный юнит уничтожен - запускаем последовательность 11, 12, 13 из текстуры peasant.bmp (0 = peasant.bmp, 11 12 13 = индексы строк). Ну и наконец, ходьба с добытым золотом (мешок за плечами). Для этого берётся перавая текстура (1 1 2 3 2 1 4 5 4), которая хранится в peasant with gold.bmp. И далее периодически прокручиваем последовательность строк 1 2 3 2 1 4 5 4. То есть: 1 2 3 2 1 4 5 4 1 2 3 2 1 4 5 4 1 2 3 2 1 4 5 4 1 2 3 2 1 4 5 4 и т.д. до тех пор, пока крестьянин идёт.

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

)индексы
номер_текстуры
номер_строки номер_строки номер_строки номер_строки, ...
номер_текстуры номер_строки номер_строки номер_строки номер_строки, ...
номер_текстуры номер_строки номер_строки номер_строки номер_строки, ...
номер_текстуры номер_строки номер_строки номер_строки номер_строки, ...

А номера текстур идут по порядку загрузки:

)спрайты
имя_файла_нулевой_текстуры_со_спрайтами
имя_файла_первой_текстуры_со_спрайтами

Обращаю ваше внимание, что необходимость в таких дополнениях связана с тем, что мы делаем движок под готовые спрайты. А спрайты хранятся специфическим образом - по несколько спрайтов в одном BMP-файле.

Для загрузки этих данных нужно добавить соответствующие переменные в тип type TOpisan=record ... end. Также необходимо написать обработчик, реагирующий на раздел "индексы".

А теперь более простое объяснение про номера строк. У нас в одном BMP файле хранится сразу несколько спрайтов. В принципе, при таком подходе на каждого персонажа игры понадобится один BMP-файл. С одной стороны это хорошо: мы вместо большого числа малнеьких текстурок грузим одну большую, и поэтому может увеличится скорость отрисовки в OpenGL, т.к. мы реже будем вызывать процедуру glBindTexture. С другой стороны, возникает необходимость генерации текстурных координат. Верхний левый угол текстуры - это 0,0. Правый нижний угол текстуры - это 1,1. Но спрайтов у нас в текстуре много. Для нахождения нужных текстурных координат в предыдущих частях было рассмотрено создание процедуры Kvadrat (смотрите файл rtsmain.pas). Вот заголовок этой процедуры:

procedure Kvadrat(XSize, YSize, sprX, sprY, strok, stolb:integer; flip:boolean);

Итак, эту процедуру мы создали как раз для того, чтобы из одной большой текстуры вывести на экран только один маленький спрайт. Например, мы загрузили текстуру peasant with gold.bmp. Мы знаем, что в этой большой текстуре содержится много маленьких спрайтов размером 72 на 72. Также нам известно количество строк и количество столбцов. Взглянем ещё раз на кусочек написанного ранее кода для этой процедуры:

glBegin(GL_QUADS);
glTexCoord2f( (0 +(stolb-1)*sprX)/XSize, 1-(sprY+(strok-1)*sprY)/YSize); glVertex2f(0,0);
glTexCoord2f( (SprX+(stolb-1)*sprX)/XSize, 1-(sprY+(strok-1)*sprY)/YSize); glVertex2f(71,0);
glTexCoord2f( (SprX+(stolb-1)*sprX)/XSize, 1-(0+(strok-1)*sprY)/YSize);glVertex2f(71,71);
glTexCoord2f( (0 +(stolb-1)*sprX)/XSize, 1-(0+(strok-1)*sprY)/YSize);glVertex2f(0,71);
glEnd;

Обратите внимание, что номера строк и столбцов мы решили считать, начиная с единицы. Итак, мы выводим спрайт. Например, спрайт из левого верхнего угла текстуры:

Kvadrat(512, 1024, 72, 72, 1, 1, false);

Эта процедура как раз и выведет маленький спрайт размером 72x72 из огромной текстуры 512x1024. Причём это будет спрайт из левого верхнего угла, так как мы задали в параметрах процедуры первую строку первый столбец. Ну а параметр false говорит о том, что нам не требуется зеркальное отражение спрайта.

Зачем вообще понадобилось зеркальное отражение? Дело вот в чём: например, крестьянин стоит, повернувшись вправо. Это спрайт. Как получить спрайт, на котором тот же крестьянин стоит, но повернулся влево? Для этого достаточно сделать зеркальное отображение отосительно оси Y (вертикальной). Более подробно код рассмотрен в предыдущих частях. Но даже если вы и сами заглянете в файл rtsmain.pas, то без труда найдёте условия проверки параметра flip:boolean, где сразу видна суть зеркального отображения. Но вот если крестьянин смотрит вперёд или назад (при виде сверху), зеркальное отражение не поможет (почему - внимательно посмотрите, что случится со прайтом при отражении относительно горизонтальной оси X).

И ещё раз проведём небольшой анализ проделанной нами ранее работы. Вначале мы получили спрайты от заброшенного проекта "Варяг". Возникла идея написать движок. Далее мы сделали всё необходимое для загрузки и отображения этих спрайтов на экран с помощью OpenGL. Потом был очень важный раздел по созданию формата файлов: мы создали формат файлов и написали код для загрузки и анализа этих данных, который выделяет нужные числовые параметры из текстового файла и заносит их в соответствующие переменные, элементы массивов и записи. Параллельно с этим мы задались такими вопросами, как: реализация карты и передвижение игрока по карте.

В следующих частях вы увидите, как проделанная нами ранее работа по написанию исходного кода облегчит создание игрового движка и самой игры. Стоит отметить, что при написании этой статьи я решил учесть некоторые замечания в адрес "Самодельного WarCraft-а". Многим из вас известен форум сайта по созданию компьютерных игр - gamedev.ru. Благодаря ценным замечаниям посетителей этого форума был выделен ряд проблем "Самодельного WarCraft-а": отсутствие разнообразия юнитов, отсутствие навыков и умений, отсутствие апгрейдов. В то же время было отмечено, что сама по себе идея создания RTS стратегической игры в реальном времени актуальна, а результаты, полученные в процессе создания "Самодельного WarCraft-а" достаточно интересны.

Итак, обозначенные выше замечания в большей или меньшей степени были учтены. При написании нового исходного кода я придерживался идей максимальной простоты и унификации. Хотя это и привело к небольшому усложнению, но всё-таки я уже дал простое решение в виде "Самодельного WarCraft-а". А в данной статье излагаю процесс более продвинутой (more advanced) версии стратегического движка.

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

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

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