Создание карты в игре, методом спрайтов
Содержание
Часть 1
Часть 2
Список ссылок
Часть 1
В играх, где используется простая 2D проекция, используется наверное два способа
построения игрового пространства (карт). Первый о котором и будет рассказ, заключается
в построение карты из спрайтов одинакового размера. А второй из уже готовых больших
изображений, но об этом в другой главе.
Преимущества этого способа - малый объем ресурсов графики, следовательно не
требовательность к ресурсам. Практически любой размер карты - он ограничен лишь
массивом и т.д. К минусам можно отнести сложность подгона текстур друг к другу.
Этот способ построения карт применялся в таких БОЕВИКАХ ( не побоюсь этого
слова ) игрового фронта как : WARCRAFT, DUNE и т.д. С помощью этого типа карт
можно написать неплохую RPG или стратегия. Ну приступим. Первое что нам нужно
это спрайты:
В примере я сделал файл Flur1.bmp там 20 спрайтов размером 32 x 32 / при создании
старайтесь делать спрайты квадратные - упрощается вычисление/. Спрайты индексируются
0 ... 19;
Что представляет из себя карта - это двух мерный массив , элементы которого -
индексы спрайтов.
Например:
MAP: Array [0..100,0..100] of integer; |
Это означает, что мы хоти создать карту размером 100 x 100 спрайтов, допустим
размер спрайта 32 x 32 тогда общая длина/ширина карты в пикселях будет 3200 x
3200. Но что вам мешает создавать карты, 1000 x 1000 клеток. ( Пример в HMM2 максимальная
карта 128 x 128 при размере спрайта 32 пикселя )
Я советую создать класс для карты, вот как сделал бы я :
type TFlur= class
private
SpriteLastIndex, // Кол-во спрайтов в Resource
SpriteWH:integer;// Размер спрайта, должен быть
квадрат
Resource:TBitmap;// Ресурсы графики
public
Table:Tlist; // Массив карты развернутый в список
Width,Heigth:Integer;// Ширина и высота карты
в спрайтах
Constructor Create(Const X,Y,Sprite:integer);//
ширина, высота, размер спрайта
Procedure LoadResource(FileName:String); //
путь к BMP ресурсу
Procedure RandomGenerator; // случайное
заполнение Table
Procedure Draw(DestHandle:TCanvas;X,Y:Integer;SourceRect:TRect);
// Canvas для вывода,
// Смещение X и Y относительно начала DestHandle
// SourceRect - какой кусок карты Table выводить на экран, В КОЛ-ВАХ
СПРАЙТОВ
Destructor Destroy; override;
end; |
По поводу Table - я использовал TList вместо Array т.к. при создании мы можем
не знаем, какого размера карту будем делать. В игре создавая один TFlur можно
подгружать туда новые карты динамически, при переходе на новый уровень ( зону,
город ... ), без замедления.
Рассмотрим процедуру Draw, т.к. все остальное достаточно понятно :
Procedure TFlur.Draw(DestHandle:TCanvas;X,Y:Integer;SourceRect:TRect);
var
I,J:integer;
dx,dy:integer; // для смещений в пикселях
TableIndex:^integer; // указатель на элемент
Table
begin
// Делаем проверку
if SourceRect.Left<0 then exit;
if SourceRect.Top<0 then exit;
if SourceRect.Left+SourceRect.Right>Heigth-1 then exit;
if SourceRect.Top+SourceRect.Bottom>Width-1 then exit;
dx:=0;dy:=0;// обнуляем смещение
// зададим цикл по области карты для показа
for I:=SourceRect.Left to SourceRect.Left+SourceRect.Right do
begin
for J:=SourceRect.Top to SourceRect.Top+SourceRect.Bottom
do
begin
TableIndex:=(Table.Items[Width*J+I]);
// Уже знакомой прцедурой копируем
один спрайт на экран
BitBlt(DestHandle.Handle,dx+X,dy+Y,SpriteWH,
SpriteWh,Resource.Canvas.Handle,TableIndex^*SpriteWh,
0,SRCCopy); inc(dy,SpriteWH);//
увеличиваем смещение dy на SpriteWH
end;
Inc(dx,SpriteWH);dy:=0;// увеличиваем смещение
dx на SpriteWH, обнуляем dy
end;
end; |
Достанем элемент из списка с координатами I,J
Так, в принципе класс написан, это даже не класс а некое подобие
ENGINE :) Что можно добавить, изменить и т.д. Все зависит от конкретной задачи:
1. Если вы хотите сделать что-то подобное Color Lines, шахмат, морского
боя и т.д. вам совсем не обязательно вводить Table - поле всегда имеет одинаковые
размеры.
2. При выполнении Flur.Draw неплохо ввести проверку на значения SourceRect
( я сделал ее внутри процедуры обработки клавиатуры).
3. Все объекты карты - дома, леса, юниты, технику и д.р. желательно
привязывать к координатам пиксельным ( да и вообще скроллинг карты надо делать
то же пиксельным - но об зтом позже ).
4. Дописать загрузку Table из файла.
5. При выводе графики на в окно происходит мерцание курсора. Это связано
с тем, что обновление курсора намного медленнее, чем вывод графики. Исправить
это можно следующим способом. С формы убирается курсор ( Form1.Cursor:=crNone;
). Координаты курсора запоминаем в двух переменных. При от рисовки делаем так
: создаем буфер, рисуем Flur в буфер, далее туда пихаем картинку из ImageList'а.
И выводим буфер на экран.Плюсы этого метода: можно сделать курсор любой формы
и менять его по ходу игры, ну и избавимся от дрожания.
Вроде все, исходник тут.
>
Хранить спрайты можно не только в BMP, но и в TImageList. При
этом появляется множество удобств:
1. Можно спрайты не вытягивать в линейку, а рисовать на прямоугольнике
- допустим 640x480. При загрузке достаточно указать размер спрайта, а ImageList
сам их разобьет.
2. Удобно добиваться прозрачности. Это может понадобится для спрайтов
не квадратной формы - ромб. Они пригодятся для изометрии.
3. Скорость вывода на экран потрясающая. Смотрите
пример.
Итак, что я переписал в TFlur : коструктор и Draw
constructor TFlur.Create(ConstMapX,MapY,SpriteW,SpriteH:integer;ResFileName:String);
// MapX,MapY - размер карты в спрайтах
// SpriteW,SpriteH - размер спрайта
// ResFileNama - путь к BMP
var
Resource:Tbitmap; // BMP с ресурсами
begin
Resource:=Tbitmap.Create; // Грузим ресурсы
из ResFileName
Resource.LoadFromFile(ResFileName);
ImageList:=TImageList.CreateSize(SpriteW,SpriteH); //
Создаем лист под спрайты
ImageList.DrawingStyle :=dsNormal; // Устанавливаем
параметры
ImageList.Masked := False;
ImageList.Add(Resource,Resource);
....
end; |
И изменения в TFlur.Draw :
....
for J:=SourceRect.Top to SourceRect.Top+SourceRect.Bottom do
begin
TableIndex:=(Table.Items[Width*J+I]);
ImageList.Draw(DestCanvas,dx+X,dy+Y,TableIndex^);
Inc(dy,32);
end;
Inc(dx,32);
....
|
При компиляции использовал компонент THeadedTimer ( инсталируйте его из директории
THeadedTimer, как установить компоненты Вы надеюсь знаете ). Этот компонент аналог
стандартного TTimer, но использованы потоки - высокая скорость работы. И последнее
- если вы будете совместно использовать Flur и спрайты то копируйте все в БУФЕР.
Т.е. карту в буфер, спрайты и курсор в буфер, буфер на экран. Тут
исходник.
Если есть у кого ссылки на ресурсы с графикой от игр ( лучше стратегий и RPG
) или вообще текстуры, но текстуры к ИГРАМ, а не web ( формат BMP, DIB, PSD) то
прошу присылать ссылки. Кто напишет что что-нибудь дельное используя эти методы
обязательно размещу. Если ВЫ знаете другие методы построения карт в играх прошу
присылать идеи.
Часть 2
Прежде чем читать данную статью, настоятельно рекомендую прочесть ее первую
часть.
В первой части я показал как можно создать карту из набора спрайтов и вывести
ее на любой TCanvas. При этом карта создавалась как таблица из наборов номеров
спрайтов, этот способ удобен там, где все объекты привязаны к координатам спрайтов.
Чтобы было более понятно, приведу простой пример:
Игра шахматы. Все фигурки привязаны к координатам доски ( у нас к координатам
карты), и их передвижение возможно только в этих координатах. Но при переносе
фигуры появляются другие координаты - промежуточные. И если ограничиваться координатами
доски не возможно создать плавного перемещения, анимации.
Для достижения плавного скроллинга карты, анимации объектов приходится вводить
координаты пиксельные.
Изменению подверглись только выделенные красным строчки.
type TFlur2= class
private
SpriteLastIndex, // Кол-во спрайтов в Resource
SpriteWH:integer;// Размер спрайта, должен быть
квадрат
Resource:TBitmap;// Ресурсы графики
Width1,Heigth1:Integer;// Ширина и высота карты
в спрайтах
public
Table:TList; // Массив карты развернутый в список
Width,Heigth:Integer;// Ширина и высота
карты в пикселях
Constructor Create(Const X,Y,Sprite:integer);//
ширина, высота карты , размер спрайта в пикселях
Procedure LoadResource(FileName:String); //
путь к BMP ресурсу
Procedure RandomGenerator; // случайное
заполнение Table
Procedure Draw(DestHandle:TCanvas;X,Y:Integer;SourceRect:TRect);
// Canvas для вывода,
// Смещение X и Y относительно начала DestHandle
// SourceRect - какой кусок карты Table выводить на экран, в пикселях
Destructor Destroy; override;
end; |
Как Вы видите изменений произошло не очень много, но зато пришлось полностью
переписать процедуру вывода готовой карты - Draw. При вызове конструктора, в качестве
параметров X и Y передается размер карты в пикселях ( ранее в количествах спрайтов
). Как и в TFlur значения X и Y должны быть равны - только квадратная карта, это
связано с особенностью хранения массива со значениями спрайтов. Еще одно условие
X должен делиться на Sprite без остатка :)
Если подробно рассматривать, что происходит в процедуре Draw, то вот основные
моменты.
1. Сначала происходит проверка, можно ли показать данные координаты.
2. Далее, путем округления в большую сторону, получаем координаты (X1,Y1,X2,Y2).
Эти координаты в системе спрайтов (т.е. 0 соответствует 0, 32 - 1, 64 - 2 ...
).
3. Получаем координаты со смещением (dx,dy)
4. Выводим спрайт в координаты (dx,dy)
В принципе на словах, это не очень понятно, но просто попробуйте нарисовать
на бумаге и я думаю Вы разберетесь.
Теперь можно задаться вопросом: а за чем все это было нужно, и чем TFlur хуже?
Все очень просто, теперь появилась возможность скроллировать
карту хоть по 1 пикселю, а раньше только по 32 ( т.е. по ширине спрайта карты
). При этом будет достигаться плавность перемещения.
И вот второй момент. Если Вы захотите поместить на карту объекты ( а Вы точно
захотите ! ), то они могут находится на любом месте карты ( и экрана соответственно
).
Чтобы поместить на нашу карту анимированные спрайты я написал небольшой класс
TSprite. Замечу что он не универсальный и далеко не совершенный. Просто для своих
целей Вы можете дописать необходимые вам пункты, но я остановлюсь на этом далее.
Вот заголовок класса TSprite:
type TSprite=Class
// Внутренние координаты спрайта, real по
тому, что
// вычисление ведется через sin и cos - достигается
// более плавное перемещение. Возможность поворота по
// любому углу.
FCoordXFl,
FCoordYFl:Real;
// "Экранные" координаты
FCoordX,
FCoordY,
FAngle:Integer;
// FAngle - вектор направления скорости спрайта,
может
// принимать значения от 0 до 360 ( 0,45,90,135 ... 360 для данной
программы)
FSpeed:Integer; // "скорость"
FCount:Integer; // Счетчик движений - всего
4 изображения анимации на каждое движение
FGo:boolean; // Стоим или идем
Imagelist:TImagelist;
constructor Create(AX,AY,AAngle:integer);
Procedure DrawNext(AСanvas:TCanvas;Dest:TRect);
Procedure Rotate(AAngle:integer);
end; |
Я думаю тут должно быть все понятно, но все же остановлюсь
на некоторых моментах.
Для чего вводятся 2 переменные на каждую координату?
Все очень просто : спрайт выводится на экран и с координатами FCoordX и FCoordY.
А FCoordXFl и FCoordYXFl нужны вот для чего: допустим спрайт движется под углом
15 градусов и его шаг 1 пиксель. При вычислении приращения его координат получаем
следующее : по оси Y приращение Cos(15)*1~0.965 а по оси X Sin(15)*1~0.25 При
округлении получаем по оси Y - 1, по оси X - 0! Вот так вроде двигаемся на северо-восток,
а приращение только по Y. Но в принципе можно использовать только Real и потом
их просто округлять.
При выполнении процедуры DrawNext передается параметр Dest:TRect - это видимая
область карты, она изображена на рисунке розовым цветом. Если координаты спрайта
не попадают в нее, то он не выводится.
Теперь остановимся на возможностях модернизации данного класса и других тонкостях.
1. Количество спрайтов на одно движение должно быть как можно больше
( в моем примере их всего 4 и видно дерганность при ходьбе героя)
2. Угол поворота в идеале 5 градусов, только тогда можно добиться очень
хорошей и плавной анимации.
3. Если Вы собираетесь писать RTS или RPG, то все параметры героя -
сила, ловкость, количество маны, жизнь и т.д. так же включают в класс.
4. Лучше всего написать один класс 'родительский' и от него делать все
дочерние. Например класс 'Static Sprite' - от него можно породить магазины, дома
героев, замки - для RPG и ангары, заводы - для RTS. Это проще чем писать отдельный
класс для каждого нового объекта. Ну не мне вас учить ООП.
5. Нужно включить контроль столкновений. При этом лучше всего проверять
сначала просто по габаритам спрайта, а потом по его маске - ведь спрайт занимает
не все пространство внутри квадрата.
6. Спрайты включаются в список ( если их больше 1 ). Тогда с ними легче
обращаться - удалять, добавлять ...Например так:
var
SpriteList:TList;
......
SpriteList.Add(TSprite.Create(100,100,35)); // пример добавления
......
TSprite(SpriteList.Items[23]).FCoordX:=100; // доступ к
спрайту 23
...... |
Оптимизация - как много в этом слове ! Конечно Вас не устроила скорость работы
данного примера. Но я и не старался это сделать - 320x320 при 10-15 кадрах это
мало. Если Вы собираетесь делать игру с разрешением 640x480 то вот способы оптимизации.
Вся загвоздка в тормознутости TCanvas - по этому обойдемся без него. Самое лучшее
использовать DirectX - смотрите раздел Lib и компонент DelphiX. И еще один вариант
- Dib, так же смотрите в разделе Lib есть пара компонентов для работы, получается
вполне приличная скорость с поддержкой всевозможных эффектов.
Дерзайте !
Вот что получилось у меня, спасибо WarLord III и Warcraft
II за предоставленные спрайты. Как переделать пример для работы со спрайтами других
размеров читайте в main.pas, там есть все необходимые комментарии и спрайты из
игры WarCraft II.
Качайте пример вот отсюда.
Список ссылок
Адрес автора |
|
Часть 1 Исходный код примера 1 |
|
Часть 1 Исходный код примера 2 |
|
Часть 2 Исходный код примера 1 |
|
Пример
работы со спрайтами: перемещение, создание, уничтожение ( смерть юнита ), перемещение
в заданную точку, атака противника... Спрайты взяты из игры Diablo. Для компиляции
потребуется DelphiX |
|
|