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

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

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

Текстуру нужно загружать только один раз. Для этого введём переменную firstrun и изменим содержимое процедуры glDraw модуля Unit1.pas следующим образом:

var firsttime:boolean=true;

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

begin

if firsttime then
begin
Zagruz;
glEnable(GL_TEXTURE_2D);
glColor3f(1,1,1);
firsttime:=false;

end;

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

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

Kvadrat;

SwapBuffers(DC);
end;

Практически сразу после запуска программы произойдёт считывание текстуры, так как сработает "if firsttime=true then". В этом блоке мы загружаем текстуру. Затем разрешаем использование текстур (OpenGL) и устанавливаем текущий цвет белым (Red=1, Green=1, Blue=1). Так как текстура уже загружена, и этот блок уже не нужно вызывать, то приравниваем firsttime значению false.

Обратите внимание, что яркость компонент цвета задаётся в диапазоне от 0 до 1. Например, синему цвету соответствует команда glColor3f(0,0,1). Красному - glcolor3f(1,0,0). Жёлтому - glColor3f(1,1,0). Чёрному - glColor3f(0,0,0). Серому - glColor3f(0.3,0.3,0.3); Тёмно зелёному - glColor3f(0,0.5,0) и т.д.

После очистки экрана вызываем процедуру рисования квадрата Kvadrat. Рассмотрим внимательнее эту процедуру:

procedure Kvadrat;
begin
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex2f(0,0);
glTexCoord2f(1,0);glVertex2f(71,0);
glTexCoord2f(1,1);glVertex2f(71,71);
glTexCoord2f(0,1);glVertex2f(0,71);
glEnd;
end;

Квадрат имеет четыре вершины, которым соответствует четыре вызова процедуры glVertex2f. Так как спрайт человечка имеет размеры 72x72 (ширина = 72, высота = 72), то мы выбрали соответствующие координаты точек. Рисуем в левом нижнем углу OpenGL-евского окна. Текстурные координаты, передаваемые на вход команде glTexCoord2f могут принимать значения от 0 до 1.

Чтобы понять, как правильно задать текстурные координаты, рассмотрим простой пример. Допустим, у нас загружена текстура с размерами 128x128. Тогда текстурные координаты рассчитываются следующим образом: u=(x+1)/128; v=(y+1)/128. Поэтому пикселю текстуры 127,127 соответствует текстурная координата 1,1. Отсчёт пикселей ведётся от 0, поэтому вместо 128 стоит число 127. Посмотрим пиксель 63, 57. Для него:

u = (63+1) / 128 = 0.5
v = (57+1) / 128 = 0.453

Теперь необходимо решить следующую задачу: в нашей текстуре "peasant with gold.bmp" содержится сразу несколько спрайтов. Необходимо составить формулы, с помощью которых можно определить крайние текстурные координаты спрайтов. Итак, текстура "peasant with gold.bmp" имеет размер 512x1024. Это означает, что пиксели имеют индексы x=0..511, y=0..1023. Текстурные координаты в OpenGL имеют значения u=0..1, v=0..1. Мы также знаем, что один спрайт в пикселях имеет размер 72x72.

Введём следующие обозначения:

XSize=512 и YSize=1024 - размеры текстуры в пикселях;
XC, YC - координаты пикселя;
U, V - текстурные координаты пикселя (измеряются от 0, поэтому "+1").

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

U = (XC+1) / XSize
V = (YC+1) / YSize

На основе этих формул устанавливаем новые текстурные координаты, позволяющие вывести самый первый спрайт из текстуры "peasant with gold.bmp":

procedure Kvadrat;
begin
glBegin(GL_QUADS);
glTexCoord2f(0,1-72/1024); glVertex2f(0,0);
glTexCoord2f(72/512,1-72/1024); glVertex2f(71,0);
glTexCoord2f(72/512,1); glVertex2f(71,71);
glTexCoord2f(0,1); glVertex2f(0,71);
glEnd;
end;

Левая нижняя координата текстуры имеет координату (u=0,v=0). Но так как первый спрайт находится у верхнего края текстуры, где координата v=1, то мы просто отступаем вниз 72 пикселя от верхнего края. Итак, верхний край текстуры - это 1. Отступаем 72 пикселя вниз, Так как это текстурные координаты, то необходимо пиксельную координату 72 поделить на размер текстуры по высоте: 72/1024. Поэтому левый нижний угол квадрата имеет текстурную координату (u=0, v=1-72/1024). Логика формирования других текстурных координат аналогична. Но после запуска программы сразу видим два недостатка: спрайт выглядит размытым, а квадрат закрашен белым цветом. Чтобы исправить первый недостаток, обратимся к содержимому модуля BMP.pas, и найдём там строки из исходного кода процедуры LoadTexture:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

Константа GL_LINEAR обозначает линейную интерполяцию между пикселями текстуры. Это и есть размывка. Чтобы убрать размывку, отключим линейную интерполяцию, и заменим её выбором "ближайших пикселей" (NEAREST PIXELS). Для этого вместо константы GL_LINEAR установим константу GL_NEAREST:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

Теперь спрайт выглядит чётко, но осталась проблема с белым фоном в форме квадрата. Чтобы решить эту проблему, достаточно сделать этот цвет прозрачным. Дело в том, что изначально эти спрайты сделаны таким образом, что этот цвет (белый) предназначен для обозначения прозрачных участков. Чтобы точно узнать значение этого цвета, воспользуемся инструментом "Color Picker" какого-нибудь графического редактора. Это может быть i.Mage или любой другой графический редактор, в котором можно узнать "RGB" компоненты цвета пикселя на картинке. Анализ с помощью программы i.Mage показал, что цвет фона спрайта не совсем белый (R=252, G=252, B=252). Белому же цвету соответствуют значения (R=255, G=255, B=255). Если представить цвет фона спрайта в OpenGL-евском исчислении, то это будет (0.988,0.988,0.988), так как 252/255=0.988.

Здесь напрашивается применение метода Color Key ("Цветовой Ключ"). Этот метод заключается в том, что OpenGL делает определённый цвет прозрачным. Так как пиксели с ключевым цветом не выводятся на экран, то они не затирают фон. В данном случае фон OpenGL-евского окна имеет чёрный цвет, поверх которого выводится белый квадрат со спрайтом.

Для реализации этого метода будем создавать в нашей текстуре альфа канал на этапе загрузки. Для этого изменим размер памяти, выделяемый в процедуре LoadBitmap модуля BMP.pas, таким образом, чтобы помимо трёх цветовых каналов присутствувал альфа-канал (четвёртый канал). Размер выделяемой памяти на данный момент определяется следующим выражением:

GetMem(pData, BitmapLength);

что необходимо заменить на

GetMem(pData, Width * Height * 4);

В каком-то смысле мы выделяем четыре прямоугольника размером Width x Height, но на деле это просто одномерный массив данных, состоящий из Width*Height*4=... байт.

Далее нам необходимо добавить генерацию альфа-канала. Это очень просто: мы в цикле проанализируем все пиксели текстуры. Если цвет пикселя равен (R=252, G=252, B=252), то в альфа канал будем заносить значение, соответствующее полной прозрачности.

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

RRR:^Byte;
GGG:^Byte;
BBB:^Byte;
AAA:^Byte;

Меняя значения этих Point-еров, мы будем выбирать нужные значения из массива pData. Также добавим вспомогательные переменные:

tData: Pointer;
J : Cardinal;

Переменная tData будет указателем на временный массив данных, в который будут "развёрнуты" данные из BMP файла. "Разворачивание" заключается в следующем: изначально из BMP фала загружается поток "RGB RGB RGB RGB RGB RGB RGB RGB...", который мы преобразуем в "RGBA RGBA RGBA RGBA RGBA RGBA RGBA RGBA ...".

Память выделим сразу для двух массивов (основного и временного):

GetMem(pData, Width * Height * 4);
GetMem(tData, Width * Height * 4);

Развернём из RGB в RGBA:

for J:=0 to 2 do
for I :=0 to Width * Height - 1 do
begin
RRR:=Pointer(Cardinal(pData) + I*3+J);
GGG:=Pointer(Cardinal(tData) + I*4+J);
GGG^:=RRR^;
end;

Скопируем данные из временного массива tData в основной pData и освободим память, занимаемую временным массивом tData:

move(tData^,pData^,Width*Height*4);
freeMem(tData);

Заполним альфа-канал массива tData таким образом, чтобы обеспечить "Color Key":

for I :=0 to Width * Height - 1 do
begin
RRR := Pointer(Cardinal(pData) + I*4);
GGG := Pointer(Cardinal(pData) + I*4 + 1);
BBB := Pointer(Cardinal(pData) + I*4 + 2);
AAA := Pointer(Cardinal(pData) + I*4 + 3);

if ((RRR^=252) and (GGG^=252) and (BBB^=252)) then
AAA^:=0
else
AAA^:=255;

end;

Так как мы изменили формат RGB на RGBA, то необходимо немного изменить параметры команд инициализации OpenGL-текстур. Для этого в строчке

gluBuild2DMipmaps(GL_TEXTURE_2D, 3, Width, Height, GL_RGB, GL_UNSIGNED_BYTE, pData);

необходимо заменить константу GL_RGB на GL_RGBA. Также заменяем число цветовых компонент с 3 (R,G,B) на 4 (R,G,B,A). После этого небольшого изменения OpenGL будет правильно интерпретировать массив данных pData:

gluBuild2DMipmaps(GL_TEXTURE_2D, 4, Width, Height, GL_RGBA, GL_UNSIGNED_BYTE, pData);

Для реализации прозрачности с использованием метода "Color Key" достаточно подать специальную OpenGL команду, которая активизирует работу с альфа-каналом. Дополнительно к этому нужно установить подходящий режим смешивания цветов (Blending Mode). После этого белый фон исчезнет, так как этот цвет мы примем за прозрачный. Блок firsttime процедуры glDraw дополним несколькими командами:

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;

При первом запуске программы, когда firsttime=true, включается режим "блендинга" (blending, смешивание цветов) с помощью команды glEnable(GL_BLEND). Затем выбирается функция смешивания цветов (команда glBlendFunc). Далее мы включаем тест альфа-канала командой glEnable(GL_ALPHA_TEST). При этом делаем видимыми все пиксели, у которых ALPHA>0.1 (т.е. ALPHA=0/255=0<0.1 будет прозрачной). Данное неравенство устанавливается командой glAlphaFunc. Теперь видно, что спрайт выводится правильно. Но это только первый спрайт, а всего в текстуре "peasant with gold.bmp" содержится 65 спрайтов (13 строк, в каждой строке по 5 спрайтов). Необходимо модифицировать нашу процедуру Kvadrat таким образом, чтобы можно было отобразить любой из этих 65 спрайтов, указав номер строки и столбца. Сейчас процедура вывода спрайта выглядит следующим образом:

procedure Kvadrat;
begin
glBegin(GL_QUADS);
glTexCoord2f(0,1-72/1024); glVertex2f(0,0);
glTexCoord2f(72/512,1-72/1024); glVertex2f(71,0);
glTexCoord2f(72/512,1); glVertex2f(71,71);
glTexCoord2f(0,1); glVertex2f(0,71);
glEnd;
end;

Данная процедура выводит спрайт из первого столбца первой строки. Выше мы ввели следующие обозначения:

XSize=512 и YSize=1024 - размеры текстуры в пикселях;
XC, YC - координаты пикселя;
U, V - текстурные координаты пикселя (отсчёт ведётся от нуля).

Введём дополнительные обозначения:

strok, stolb - номер строки и столбца (отсчёт ведётся от единицы);
sprx, spry - размеры спрайта по горизонтали и вертикали (для крестьянина это 72x72).

Для спрайта из первого столбца первой строки никаких манипуляций с текстурными координатами, которые уже записаны в процедуре Kvadrat, делать не нужно. Отсюда следует, что при написании новых формул от значений strok и stolb следует отнять по единице (stolb-1, strok-1). Сделаем соответствующие изменения и заменим числа на параметры (переменные), чтобы номер спрайта можно было выбирать указанием строки и столбца на входе процедуры Kvadrat:

procedure Kvadrat(XSize, YSize, sprX, sprY, strok, stolb:integer);
begin
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;
end;

Здесь всё достаточно просто: в скобках рассчитываются пиксельные координаты, после чего они делятся на XSize или YSize и получаются текстурные координаты (нормированные координаты). Для реализации смещения по строкам и столбцам мы просто умножаем размер спрайта на номер строки (strok) и столбца (stolb). И, наконец, нули в скобках представляют собой вершины квадрата, расположенные диаметрально противоположно вершинам со sprX и sprY в скобках.

Стоит отметить, что выражение 1-(sprY+(strok-1)*sprY)/YSize эквивалентно выражению (YSize-(sprY+(strok-1)*sprY))/YSize. Отсюда сразу понятен смысл данного выражения: мы отступаем от верхней границы текстуры вниз на (sprY+(strok-1)*sprY). Вообще понять текстурные координаты (нормированные координаты) очень просто, если воспользоваться пропорциями. При этом принимается, что XSize - это единица, а дальше идёт обыкновенная пропорция: если XSize - это единица, то XC - это ... Просто по формулам. То же касается и YSize.

Итак, создана достаточно простая и удобная процедура, позволяющая выводить спрайт. В параметрах процедуры мы задаём размеры текстуры (XSize, YSize), размеры спрайта (sprX, sprY) и индекс спрайта (номер строки и столбца). Например, чтобы вывести спрайт из правого нижнего угла текстуры, мы должны в процедуру glDraw добавить следующую строчку:

Kvadrat(512,1024,72,72,13,5);

Здесь всё понятно: 512x1024 - это размеры текстуры; 72x72 - это размеры спрайта; 13x5 - это номер строки (13) и столбца (5). В итоге на экран выводится нужный спрайт.

Так как спрайты сделаны только для половины диапазона углов 0..360 градусов, то вторую половину можно сделать с помощью зеркального отражения или так называемого flip-а. Это очень просто: достаточно поменять текстурные координаты по оси X местами (выбор горизонтальной оси связан с тем, что все спрайты "смотрят" вправо). Добавим такую возможность к нашей процедуре Kvadrat:

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

if (flip=true) then
begin
glBegin(GL_QUADS);
glTexCoord2f( (SprX+(stolb-1)*sprX)/XSize, 1-(sprY+(strok-1)*sprY)/YSize); glVertex2f(0,0);
glTexCoord2f( (0+(stolb-1)*sprX)/XSize, 1-(sprY+(strok-1)*sprY)/YSize); glVertex2f(71,0);
glTexCoord2f( (0+(stolb-1)*sprX)/XSize, 1-(0+(strok-1)*sprY)/YSize);glVertex2f(71,71);
glTexCoord2f( (SprX+(stolb-1)*sprX)/XSize, 1-(0+(strok-1)*sprY)/YSize);glVertex2f(0,71);
glEnd;
end
else
begin
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;
end;

end;

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

Kvadrat(512,1024,72,72,13,5,true);

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

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

С чего начать написание движка стратегической игры? Этот вопрос был рассмотрен мною в статье "Как сделать 2d игру: уровни и монстры, а также скрипты, интерпретаторы, искуственный интелект и создание стратегических игр". В этой статье я дал базовые вещи для написания любых игр, в том числе и стратегических.

Первым делом нужно определиться с тем, какие основные типы необходимо задать в нашей игре. Для всех юнитов мы сделаем один главный тип. Это относится и к подвижным, и к неподвижным юнитам. Чтобы движок был более универсален мы не будем создавать отдельный тип для ресуров. Поэтому ресурсы типа леса, золота и т.п. мы объеденим вместе с юнитами. Так мы облегчим модернизацию движка в будущем. Когда я здесь пишу о типе, я говорю в смысле программирования ("type ...").

Карта будет представлять собой обычную сетку - двухмерный массив. В зависимости от значения элемента массива будет отображаться тот или иной тайл (трава, вода, песок и т.п.). Для деревьев следует предусмотреть возможность выравнивания по сетке, чтобы они состыковывались с тайлами травы. К постройкам выравнивание можно не применять.

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

Реализация интерпретатора мыслей, наделение юнитов мыслями и другие элементы, из которых складывается движок стратегической игры, достаточно проста. Мысли - это простые текстовые выражение, разбор которых осуществляется интерпретатором мыслей. Интерпретатор мысли представляет собой простую систему условий, реализованных оператором case или несколькими if. Также мы будем отслеживать некоторые события. Например, касание юнитов (касание описанных окружностей). Зрение юнитов обеспечится проверкой касания окружностей, имеющих больший радиус, чем сам юнит.

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

Также необходимо написать исходный код, который позволит реализовать ресурсы, добычу ресурсов, силу атаки, энергию удара, цену юнитов и объектов, апгрейды. Всё это достаточно просто, если делать всё последовательно и учитывать основновные принципы, которые я изложил в статьях: "Основная идея создания компьютерных игр.", "Создание летающих пулек.", "Как устроены 2d бродилки и создание искуственного интелекта (AI)." и др..

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

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