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

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

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

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

Попбробуем описать юнит "крестьянин". Нам необходимо описать следующие элементы:

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

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

  • индексы спрайта в неподвижном состоянии
  • диапазон индексов спрайтов анимации ходьбы
  • диапазон индексов спрайтов анимации добычи ресурсов
  • диапазон индексов спрайтов атаки
  • описанный радиус (для проверки столкновений между окружностями)

Создание неподвижных зданий при такой структуре описания юнитов делается очень просто: для этого нужно сделать пустой запись "скорость передвижения". Итак, мы сформировали облик файлов с данными, описывающих юниты. Перед тем, как перейти к написанию исходного кода, попробуем набросать примерное содержание текстового файла с описанием юнита "крестьянин". Разделы будем начинать со знака ")", а подразделы - со знака "]":

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

)энергия
жизнь 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

Файл описания золотника будет выглядеть следующим образом:

)название
золото золотник

)энергия
жизнь 100 1

)ресурс
100000

Это означает, что золотник имеет уровень энергии 100. Скорость самовосстановления - 100/1=100 единиц энергии в секунду. Ресурс золота составляет 100000 единиц.

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

massiv: array[0..100] of integer;

мы будем использовать конструкцию вида:

massiv: array of integer;
...
setlength(massiv,101);

Раздел USES модуля rtsmain.pas дополним библиотеками "_strman, sysutils":

uses BMP,OpenGL,_strman,sysutils;

Для хранения данных об энергии зададим тип TEnergia:

type TEnergia=record
nazv :string;
max :single;
vosst :single;
end;

Названия записей выбраны таким образом, чтобы было легко догадаться об их назначении: nazv - название энергии; max - максимальный уровень энергии; vosst - время восстановления от 0 до макимального уровня. Скорость восстановления определяется следующим образом: V=max/vosst.

Далее задаём тип описания ударов:

type TUdar=record
nazv :string;
chto :string;
sila :single;
treb_energ :string;
rash_energ :single;
end;

В начале идёт nazv - название удара; chto - в этой строке записывается название уменьшаемой в результате удара энергии; sila - сколько единиц отнимает удар; treb_energ - название требуемой энергии для удара; rash_energ - расход требуемой энергии для удара.

Теперь определяем тип для добычи:

type TDobicha=record
nazv :string;
kolvo :single;
vrem :single;
end;

Здесь nazv - название добываемого ресурса; kolvo - количество одной порции; vrem - время добычи одной порции ресурса.

type TCena=record
nazv :string;
kolvo :single;
vrem :single;
end;

Здесь nazv - название расходуемого при покупке ресурса; kolvo - цена, измеряемая в единицах ресурса; vrem - время расходования ресурса.

Для описания количества объектов задан следующий тип:

type TKolvo=record
nazv :string;
kolvo :single;
end;

С помощью этого типа мы можем задавать количество kolvo какого-нибудь объекта с названием nazv.

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

type TUpgrade=record
nazv :string;
cena :array of TCena;
kolvo :array of TKolvo;
end;

Используя в качестве базы описанные выше типы, запишем описание юнита:

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;

Здесь nazv - это название юнита для движка (внутреннее название); nazvdisp - это название, которое будет показываться на дисплее компьютера во время игры; cena и neobh - это списки того, что требуется для покупки (создания) данного юнита; resurs - признак того, что юнит является ресурсом (золото, дерево и т.п.); radius - радиус юнита для алгоритма проверки столкновений.

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

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

В игре будут разные рассы, поэтому понадобится ещё один массив:

var Rassa:array of TRassa;

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

Например, название раздела определяется следующим образом:

if s[1]=')' then razdel:=StringWordGet(s, ')',1)

Тогда логично, что подраздел и его названия можно определить так:

if s[1]=']' then podrazdel:=StringWordGet(s, ']',1)

В дальнейшем занесение данных в массивы осуществляется за счёт разбора названий разделов и подразделов:

if razdel='название' then
begin
SetLength(fRassa.Opisan,
length(fRassa.Opisan)+1);
i:=length(fRassa.Opisan)-1;
fRassa.Opisan[i].nazv:=StringWordGet(s, ' ',1);
fRassa.Opisan[i].nazvdisp:=StringWordGet(s, ' ',2);

fRassa.Opisan[i].resurs:=false;
end;

Чтение из файла мы осуществляем простым readln(f,s), а сама переменная f имеет тип text (f:text). Данные организованы следующим образом: есть несколько файлов с описанием юнитов данной рассы и один главный "индексный" файл, в котором перечислены имена этих файлов:

krestyanin.txt
voin.txt
katapulta.txt
baza.txt
ferma.txt
barrak.txt

Поэтому помимо процедуры загрузки описания юнита (ZagruzOpisan) необходимо сделать процедуру загрузки рассы (ZagruzRassa):

procedure ZagruzRassa(fname,fpath:string);
var f:text;
s:string;
begin
assignFile(f,fpath+fname);
reset(f);

SetLength(Rassa,Length(Rassa)+1);

repeat
readln(f,s);
zagruzOpisan(fpath+s,Rassa[length(rassa)-1]);
until eof(f);

closeFile(f);
end;

Эта процедура последовательно загружает описания различных юнитов, расширяя массив для данной рассы. Данные о спрайтах для каждого юнита мы добавим немного позже. А сейчас ещё раз посмотрим на структуру хранения описаний юнитов. Файл с описанием - это обычный текстовый файл, в котором данные разделены на разделы: ")название", ")энергия", ")скорость", ")удар" и т.д.. Разделы и подразделы мы обозначаем скобкой (разделы - круглой, подразделы - квадратной). Если в файле отсутствует какой-либо раздел, то соответствующий этому разделу массив будет иметь нулевую длину (пустой). Загрузчик описаний сделан таким образом, что последовательность разделов в файле не имеет значения. Если в описании присутствует раздел ")ресурс", то юниту делается соответствующая пометка (fRassa.Opisan[i].resurs:=true). Такая структура файлов очень удобна. Например, мы можем задать базу следующим образом: база это неподвижный объект, поэтому раздел ")скорость" отстутствует (кстати, никто не мешает сделать подвижную базу, или водоплавающую). Дальше: база может создавать крестьян; это умение мы записываем в раздел "стройка". Именно так. Причём здсь будет всё то же, что и в Warcraft 2: при строительстве крестьянина сработает соответствующая проверка удовлетворения требований раздела ")цена" по количеству имеющихся ресурсов и прочих объектов (ферм). Считывание строковых и числовых данных осуществляется с помощью стандартных функций и библиотеки STRMAN. Например, загрузка данных из подраздела ")ресурсы" раздела ")цена":

if (razdel='цена') and (podrazdel='ресурсы') then
begin
SetLength(fRassa.Opisan[i].cena,
length(fRassa.Opisan[i].cena)+1);
j:=length(fRassa.Opisan[i].cena)-1;

fRassa.Opisan[i].cena[j].nazv:=StringWordGet(s, ' ',1);
fRassa.Opisan[i].cena[j].kolvo:=StrToFloat(StringWordGet(s, ' ',2));
fRassa.Opisan[i].cena[j].vrem:=StrToFloat(StringWordGet(s, ' ',3));
end;

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

Загрузка и парсинг (parsing, "разбор содержимого") файлов с описанием юнитов начинается с того, что мы открываем для чтения файл fname:

assignFile(f,fname);
reset(f);

Далее идёт идёт цикл вида:

repeat
...
until eof(f);

В этом цикле и происходит разбор содержимого файла (парсинг файла). Внутри цикла мы при каждом проходе осуществляем считывание текстовой строки из фала. Считывание продолжается до тех пор, пока не будет достигнут конец файла (функция eof). Так как идентификатор файла f имеет тип text (f:text), то считывание строк осуществляется простой процедурой readln:

readln(f,s);
s:=trim(s);

Затем мы проверяем, есть ли что-нибудь в этой строке. Это необходимо, так как строка может оказаться пустой. Если в считанной строке s есть какие-либо символы, то начинается блок "begin-end" по соответствующему "if"-у:

if s<>'' then
begin
...
end;

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

if s[1]=')' then razdel:=StringWordGet(s, ')',1)
else
if s[1]=']' then podrazdel:=StringWordGet(s, ']',1)
else
begin
...
end;

В блоке "begin-end", который начинается после второго "else", идёт анализ параметров. Например, если текущий раздел имеет наименование "удар", то разбор параметров удара идёт следующим образом:

if razdel='удар' then
begin
SetLength(fRassa.Opisan[i].udar,
length(fRassa.Opisan[i].udar)+1);
j:=length(fRassa.Opisan[i].udar)-1;

fRassa.Opisan[i].udar[j].nazv:=StringWordGet(s, ' ',1);
fRassa.Opisan[i].udar[j].chto:=StringWordGet(s, ' ',2);
fRassa.Opisan[i].udar[j].sila:=StrToFloat(StringWordGet(s, ' ',3));
fRassa.Opisan[i].udar[j].treb_energ:=StringWordGet(s, ' ',4);
fRassa.Opisan[i].udar[j].rash_energ:=StrToFloat(StringWordGet(s, ' ',5));

end;

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

StringWordGet('слово1;слово2;слово3;слово4;слово5', ';' , 3) = 'слово3'

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

Когда цикл по считыванию строк из файла отработает (функция eof(f)=true), то необходимо закрыть файл:

closeFile(f);

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

К настоящему моменту у нас имеются следующие элементы:

  • Основа (framework) для создания OpenGL окна и вывода графики
  • Процедуры для загрузки и вывода спрайтов с учётом alpha-канала (прозрачность)
  • Предварительная версия формата файлов описания юнитов, объектов и ресурсов
  • Загрузчик файлов описания юнитов

И уже есть возможность попробовать вывести анимированный спрайт:

procedure TForm1.glDraw(); // главная процедура рисования в OPENGL окне

begin

if firsttime then
begin
Zagruz;

glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0.1);

glColor3f(1,1,1);
firsttime:=false;
end;


glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT); // очищаем экран
glLoadIdentity(); // заполняем матрицу единичной диагональной

// здесь добавим команды для вывода на экран

Kvadrat(512,1024,72,72,round(c),round(u),false);

c:=c+0.1;
if c>13 then c:=1;

u:=u+0.01;
if u>5 then u:=1;

SwapBuffers(DC);
end;

В блоке firsttime мы загружаем спрайт, а потом каждый кадр выводим на экран различные кадры спомощью процедуры Kvadrat.

Как видите, уже готова хорошая база для начала разработки главного игрового кода. А как вы понимаете, это сделать очень просто. Вы можете убедиться в этом, если посмотрите на проект "Самодельный WarCraft".

Выше я уже писал, что для каждой рассы существует свой список описаний юнитов:

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

Был задан массив расс:

var Rassa:array of TRassa;

В описанных выше массивах задаются "базовые" типы. Для экономии памяти целесообразно создать общий массив юнитов, в котором будут храниться изменяющиеся параметры (энергия, ресурсы, положение, угол поворота). Для установления взаимосвязи между массивом Opisan и массивом Obyekti

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;

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

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