Довольно часто можно встретиь, использующие изометрическую проекцию. Это нечто среднее между 2D и 3D,
поэтому такие игры довольно часто называют 2.5D. Вы, конечно же, знаете такие игры; это Diablo, Railroad Tycoon,
Fallout и многие другие. Камера в такой проекции находится сверху и сбоку сцены, и лишена эффекта кошачьего
глаза, т.е параллельные линии остаются параллельными. Чаще всего 2.5D игры делают с помощью 2D API -
DirectDraw например. Но там сделана система координат, связанная с поверхностью экрана, а нам нужна
полу3Dэшная система, поэтому все преобразования придется делать самому. О том как это сделать и пойдет
речь в этой главе.
Первое, что нужно сделать - подготовить изображения. Наиболее оптимальным решением будет использовать
изображения с соотношением длины и высоты равным 2:1. Также необходимо чтобы и длина и высота делились
без остатка на 2. Лучше используйте спрайты размерами 60х30 или 64х32. Я использовал спрайты размером
60х30 и вот как они выглядели:
Изображения готовы - приступаем к программированию. Казалось бы все просто: в зависимости от
того какой объект находится в определленой ячейке массива карты, рисуешь определеный спрайт. Но
не тут-то было: координаты то разные. В массиве обычные - прямоугольные, а на экране должны быть
кривые - косоугольные. Для сравнения приведу картинки этих самых систем:
Нужна формула преобразования. Копошился я два дня в инете: кроме формул,
которые не помещаются в две строки ничего не нашел. Пришлось выводить самому. Оказалось все
элементарно. Методом научного тыка удалось установить следующее. Пусть X и Y - это координаты
прямоугольной карты (нашего массива), а U и V - это уже координаты косоугольной системы переведенные
на экран. Тогда формулы принимают такой вид:
u = x-y + (W-1)
v = x+y
Здесь W - это ширина нашей мэпы. Почему W-1, потому что нумерация клеток идет не с 1, а с 0.
Все просто, не правда ли. Если хотите понять откуда взялись эти выражения нарисуйте на листочке
рядом две системы: прямую и кривую :), и внимательно посмотрите на них. У меня просто не
хватает слов, чтобы объяснить это. Иногда может понадобиться обратный перевод: из косоугольных
координат в прямые. Методом сложения и вычитания выведенных формул получаем искомые:
x = (u+v - W+1) / 2
y = (v-u + W-1) / 2
Теперь о том, как будет выглядеть на практике рисование нашей карты:
void CClassGraphic::DrawLand(MAP *pMap)
{
UINT u, v, nTile;
for (int col=0; col<MC_W; col++)
for (int row=0; row<MC_H; row++)
{
nTile = pMap->Obj[col][row];
if (nTile != MB_NOTHING)
{
// IL_W - это ширина спрайта
// IL_H - его высота
u = (col-row+MC_W-1)*(IL_W / 2);
v = (row+col)*(IL_H / 2);
ddsBack->BltFast(u, v-nTilesOffset[nTile],
ddsImg, (RECT*)&rcTiles[nTile],
DDBLTFAST_SRCCOLORKEY);
}
}
}
Координаты U и V умножаются на половины соответствующих размеров спрайта потому, что расстояние
между соседними клетками равно именно половине соответствующих размеров. "Как же так?" -
спросите вы - "Почему они не наежают друг на друга?". Все просто: рисование изометрической
карты идет как бы через клетку. Именно поэтому карты хранят в прямоугольных массивах - чтобы попусту
не терять половину памяти (хотя скорее из-за того, что так удобнее). Теперь о том почему вторым
параметром функции BltFast является v-nTilesOffset[nTile], а не просто v. Это сделанно для того, чтобы
корректно отображать объекты высота которых больше стандартной (высоты спрайта), колоны например.
Координата V поднимается на величину равную разнице высоты текущего спрайта и стандартного спрайта.
Таким образом рисуемое изображение оказывается как раз где надо. Просто заранее следует подготовить
массив, в котором будут храниться значения смещения каждого спрайта. Зачастую необходимо отобразить
объекты занимающие на карте не одну клетку, а много. Например нам нужно отобразить вот такую
штуковину:
Как известно, спрайт нужно рисовать из левого верхнего угла. Вследствие поворота изометрической системы,
крайней левой частью изометрического изображеия будет на самом деле являться левая нижняя часть
прямоугольного изображения. Непонятно? - вернитесь к изображению алтаря: оно состоит из 9 частей (3x3),
при этом читающий книгу будет повернут лицом к северу. Ну теперь думаю понятно. Так вот, механизм
такой: на прямоугольной карте нижнюю левую часть обозначить как этот большой объект, а все остальные
квадраты, входящие в этот объект обозначить как MB_NOTHING (то бишь ничего нету, дырка :). Тогда
в цикле рисования мэпы объект нарисуется и после не будет ничем перекрываться. Что еще может
понадобиться при написании Diablo III :) ? Конечно же, определение позиции курсора в изометрической
системе. Что мы можем получить от Windows? Да только экранные координаты курсора. Будем переводить
в этом помогут маски. Создайте что-нибудь похожее на такую картинку:
Главное, чтобы вы точно знали цвета по краям. ВНИМАНИЕ: ни в коем случае не используйте Photoshop
для создания масок: он сохраняет файлы с искожением, погрешность в 1 единицу цвета вам гарантированно.
Я из-за него потерял 2 дня: ничего не работало, а я искал ошибку в коде. Оказывается синий, например,
был не RGB(0,0,255), а RGB(0,0,254). А по сему для создания масок используйте старый, милый PaintBrush.
Так с софтом разобрались - возвращаемся к делу. Целочисленным делением координаты мыши
на соответствующий стандартный размер определяем приблизительные координаты клетки в которую
попал курсор, вернее координаты прямоугольной на экране клетки. Далее нахождением остатка от
деления находим координаты курсора относительно той самой прямоугольной области, берем из маски
пиксель с такими координатами и в зависимости от цвета смещаем координаты и получаем окончательный
результат. Чтобы окончательно разобраться взгляните на иллюстрацию.
При необходимости можно перевести изо координаты в координаты карты, что я и сделал.
Таким образом процедура для расчета перемещения курсора будет выглядеть примерно так:
...
hMaskBm = (HBITMAP)LoadImage(
NULL, "LandSelMask.bmp", IMAGE_BITMAP, IL_W, IL_H,
LR_LOADFROMFILE|LR_CREATEDIBSECTION);
hMaskDC = CreateCompatibleDC(NULL);
SelectObject(hMaskDC, hMaskBm);
...
void CClassGame::CalcMouse()
{
const int u = (int)(mouseX / IL_W);
const int v = (int)(mouseY / IL_H);
m_scene.Map.MouseX = (int)(((u+v)*2-MC_W+1)/2);
m_scene.Map.MouseY = (int)(((v-u)*2+MC_W-1)/2);
COLORREF color;
color = GetPixel(hMaskDC, mouseX % IL_W, mouseY % IL_H);
if (color == RGB(255, 0, 0)) m_scene.Map.MouseX--;
if (color == RGB(0, 255, 0)) m_scene.Map.MouseY++;
if (color == RGB(0, 0, 255)) m_scene.Map.MouseX++;
if (color == RGB(255,255,0)) m_scene.Map.MouseY--;
}
Ну вот, в принципе, и все.
Можете писать свой собственный Baldurs Gate. Для начала я подготовил небольшой
изодвижок - каркас для работы.