netlib.narod.ru< Назад | Оглавление | Далее >

Редактирование и хранение блоков

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

Хранение блоков в двухмерном массиве

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

// Установка размеров карты
#define TilesWide   10
#define TilesHigh   10
// Объявление массива с картой
int iTileMap[TilesWide][TilesHigh];
// Заполнение всей карты блоком с номером 0
memset(&iTileMap, 0, (TilesWide * TilesHigh) * sizeof(int));

Как видите, в приведенном фрагменте кода объявлен двухмерный массив целых чисел. Поскольку и ширина и высота карты составляют 10 блоков, вся карта содержит 100 блоков. Координаты верхнего левого блока равны 0, 0, а координаты нижнего правого блока — 99, 99. Такая карта изображена на рис. 5.31.


Рис. 5.31. Блочная карта из 100 элементов

Рис. 5.31. Блочная карта из 100 элементов


В приведенном выше фрагменте кода я очищаю карту, заполняя ее блоками с номером 0. Это общепринятая практика, поскольку блок с номером 0 является базовым блоком для визуализации. Обычно этот блок содержит изображение земли или даже является специальным блоком-пустышкой. Блок-пустышка — это обычный блок, на котором расположена предупредительная надпись, например «НЕ ИСПОЛЬЗУЕТСЯ». Наличие такого блока позволяет сразу увидеть те области карты, блоки которых не были инициализированы.

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

iTileMap[2][3] = 15;

Все, что надо сделать — присвоить желаемое значение расположенному в требуемой позиции элементу массива.

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

// Сверху вниз
iTileMap[0] [0] = 15;
iTileMap[0] [1] = 15;
iTileMap[0] [2] = 15;
iTileMap[0] [3] = 15;
iTileMap[0] [4] = 15;
// Слева направо
iTileMap[1] [4] = 15;
iTileMap[2] [4] = 15;
iTileMap[3] [4] = 15;
// Снизу вверх
iTileMap[3] [3] = 15;
iTileMap[3] [2] = 15;
iTileMap[3] [1] = 15;
iTileMap[3] [0] = 15;
// Справа налево
iTileMap[2] [0] = 15;
iTileMap[1] [0] = 15;

Ответ показан на рис. 5.32.


Рис. 5.32. Карта с составленной из блоков буквой О

Рис. 5.32. Карта с составленной из блоков буквой О


Если вы решили, что теперь на карте находится составленное из блоков изображение буквы «О» или цифры 0, похвалите себя. Код начинается с рисования линии, образующей левую грань буквы О, начинающуюся сверху и заканчивающуюся внизу. Следующий блок кода рисует нижнюю чась буквы О слева направо. Затем код рисует правую часть буквы О снизу вверх. И, наконец, код завершает рисование буквы О, проводя линию справа налево.

Не кажется ли вам, что вы уже где-то это видели? Вполне возможно, если вы раньше рисовали какое-либо избражение по точкам. Если смотреть в корень, рисование изображения по точкам весьма похоже на создание блочной карты.

Хранение многослойных блоков

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

// Установка размеров карты
#define TilesWide   10
#define TilesHigh   10
#define TileLayers  3

// Объявление массива для хранения карты
int iTileMap[TilesWide][TilesHigh][TileLayers];
// Заполнение всей карты блоком с номером 0
memset(&iTileMap, 0, (TilesWide * TilesHigh * TileLayers) * sizeof(int));

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

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

Последнее изменение кода относится к инициализации массива. Раз у вас несколько слоев, вам требуется очистить больше элементов массива.

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


Рис. 5.33. Блочная карта с двумя слоями

Рис. 5.33. Блочная карта с двумя слоями


Первый слой карты содержит два типа блоков с изображанеием травы. Второй слой карты содержит блоки с изображением камней. Взгляните на код, предназначенный для генерации такой карты:

//
// Установка базовых блоков
//

// Вертикаль
for(int i = 0; i < 10; i++) {
   // Горизонталь
   for(int j = 0; j < 10; j++) {
      // Случайный выбор базового блока
      iTileMap[i][j][0] = rand() % 2;
   }
}

//
// Добавляем блоки с деталями
//

iTileMap[5][5][1] = 3;
iTileMap[3][9][1] = 3;
iTileMap[1][7][1] = 3;
iTileMap[8][8][1] = 3;
iTileMap[6][3][1] = 3;
iTileMap[4][1][1] = 3;

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

В следующем слое вручную размещаются блоки с изображением камней. Положение блока в массиве определяется заданными координатами [x][y][z], где координата z отвечает за слой, в котором будет находиться блок. Задавая значение z равным 1, мы указываем игре, что изображения камней помещаются во втором слое.

Создание класса для представления блоков

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

Заголовок класса

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

class TileClass
{
   private:
      int*m_iValue;
      intm_iNumLayers;
      float *m_fRotX;
      float *m_fSize;

   public:
      TileClass();
      ~TileClass();
      int iGetValue(int layer);
      void vSetValue(int value, int layer);
      float fGetRot(int layer);
      void vSetRotation(float fRot, int layer);
      float fGetSize(int layer);
      void vSetSize(float fSize, int layer);
      void vSetNumLayers(int layers);
};

В классе есть четыре закрытых члена. Первый из них, m_iValue, хранит значение блока. Предположим, у вас есть 1000 загруженных в память растровых изображений, предназначенных для рисования блоков. Значение равное 1, указывает, что для блока используется первое загруженное в память растровое изображение. Таким образом, значение блока — это ни что иное, как индекс в массиве растровых изображений.

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

Следующим в поле нашего внимания попадает член класса с именем m_fRotX. Эта переменная определяет угол поворота рассматриваемого блока. Она действительно полезна, чтобы добавить вашим картам разнообразия не добавляя нового содержимого. Чтобы создать полностью новую графику вам достаточно просто повернуть блок на 90 или больше градусов. Я использую здесь значение с плавающей точкой только потому, что в последнее время работаю исключительно с трехмерной графикой. Если вы создаете библиотеку для двухмерной блочной графики, то можете использовать здесь целое число.

Теперь мы переходим к члену данных m_fSize. Эта переменная содержит размер блока. Для трехмерного мира размер измеряется в единицах трехмерной системы координат. Для двухмерного мира размер задается в пикселах. Если вы используете блоки 64 x 64 точки, размер будет равен 64. Обратите внимание — я предполагаю, что блоки будут квадратными. Если вы решите использовать прямоугольные блоки, вам придется использовать для хранения их размера две переменные, например m_fSizeX и m_fSizeY.

Переменные класса уже продефилировали перед вами, так что теперь настало время для функций класса. Первые две функции, которые вы видите — это конструктор и деструктор класса. В них нет ничего специального — обычная чепуха С++.

Первая заслуживающая отдельного упоминания функция класса называется iGetValue(). Она применяется для получения значения указанного слоя блока. Это наиболее часто вызываемая функция класса, поскольку обращение к ней происходит каждый раз, когда рисуется данный блок карты. Приведенная в примере функция возвращает при вызове значение целочисленной переменной m_iValue. Я предпочитаю этот метод, поскольку использую возвращаемое число в качестве индекса в моем массиве данных блоков. Вы можете захотеть, чтобы функция возвращала дескриптор растрового изображения, структуру данных или какой-либо другой тип данных, используемых в вашей библиотеке.

Следующая функция класса, vSetValue(), получает два параметра. Первый параметр является целым числом, определяющим значение блока, которое вы хотите установить. Второй параметр задает слой в котором будет установлено это значение. Значения хранятся в переменной класса m_iValue.

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

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

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

Следующая функция, vSetSize(), получает два параметра. Первый параметр устанавливает размер блока в единицах трехмерной системы координат или в пикселах (для двухмерной графики). Второй параметр указывает слой, к которому эта информация относится. Полученные значения сохраняются в переменной класса m_fSize.

Последняя, но не менее важная функция класса — это vSetNumLayers(). Ей передается единственный параметр с именем layers. Главное назначение этой функции — установка количества слоев блока для хранения номеров растровых изображений, углов поворота и размеров.

Вот и все, что можно сказать о заголовке класса. Структура класса показана на рис. 5.34.


Рис. 5.34. Структура класса для блочной графики

Рис. 5.34. Структура класса для блочной графики


Реализация класса

Приведенный ниже фрагмент кода содержит реализацию класса для блочной графики.

#include "TileClass.h"

// Конструктор
TileClass::TileClass()
{
   // Инициализация внутренних переменных
   m_iNumLayers = 0;
   m_iValue = NULL;
   m_fRotX = NULL;
   m_fSize = NULL;
}
// Деструктор
TileClass::~TileClass()
{
   // Освобождаем буфер слоев, если он был выделен
   if(m_iValue)
      delete [] m_iValue;
   if(m_fRotX)
      delete [] m_fRotX;
   if(m_fSize)
      delete [] m_fSize;
}
// Установка количества слоев
void TileClass::vSetNumLayers(int layers)
{
   // Освобождаем ранее выделенные буферы слоев
   if(m_iValue)
      delete [] m_iValue;
   if(m_fRotX)
      delete [] m_fRotX;
   if(m_fSize)
      delete [] m_fSize;
   // Выделяем память для буфера слоев
   m_iValue = new int[layers];
   memset(m_iValue, 0, layers * sizeof(int));

   m_fRotX = new float[layers];
   memset(m_fRotX, 0,layers * sizeof(int));

   m_fSize = new float[layers];
   memset(m_fSize, 0,layers * sizeof(int));

   // Устанавливаем количество слоев
   m_iNumLayers = layers;
}
// Получение значения блока
int TileClass::iGetValue(int layer)
{
   // Проверяем правильность указанного номера слоя
   if(layer >= m_iNumLayers) {
      return(-1);
   }
   // Возвращаем значение
   return(m_iValue[layer]);
}
// Установка значения блока
void TileClass::vSetValue(int value, int layer)
{
   // Проверяем правильность указанного номера слоя
   if(layer >= m_iNumLayers) {
      return;
   }
   // Устанавливаем значение
   m_iValue[layer] = value;
}
// Установка угла поворота
void TileClass::vSetRotation(float fRot, int layer)
{
   // Проверяем правильность указанного номера слоя
   if(layer >= m_iNumLayers) {
      return;
   }
   m_fRotX[layer] = fRot;
}
// Установка размера блока
void TileClass::vSetSize(float fSize, int layer)
{
   // Проверяем правильность указанного номера слоя
   if(layer >= m_iNumLayers) {
      return;
   }
   m_fSize[layer] = fSize;
}
// Получение угла поворота 
float TileClass::fGetRot(int layer)
{
   // Проверяем правильность указанного номера слоя
   if(layer >= m_iNumLayers) {
      return(-1.0f);
   }
   return(m_fRotX[layer]);
}
// Получение размера блока
float TileClass::fGetSize(int layer)
{
   // Проверяем правильность указанного номера слоя
   if(layer >= m_iNumLayers) {
      return(-1.0f);
   }
   return(m_fSize[layer]);
}

Первой реализованной функцией является конструктор класса. В нем выполняется инициализация некоторых переменных класса. Внутри функции я устанавливаю количество слоев, равным 0. Я также присваиваю указателям на значения, размеры и углы поворота константу NULL. Это сделано для того, чтобы предотвратить случайное освобождение не выделенной ранее памяти, вызванное тем, что при создании экземпляра переменным могут быть присвоены случайные значения.

Следующая функция — это деструктор класса. Она просто проверяет была ли выделена какая-нибудь область памяти, и, если да, то освобождает ее.

Затем следует код функции TileClass::vSetNumLayers(), устанавливающей количество слоев блока. Поскольку вы можете указать в качестве количества слоев любое осмысленное число, главная задача этой функции заключается в выделении необходимой для каждого слоя памяти. Сначала функция освобождает выделенную ранее память. Затем она выделяет память для переменных класса m_iValue, m_fRotX и m_fSize. Как только эта задача выполнена, выделенная память заполняется нулями с помощью вызова функции memset(). Помните, что эта функция должна быть вызвана перед первым использованием объекта блока. Если вы попытаетесь получить данные несуществующего слоя, класс вернет ошибку.

Далее следует наиболее часто вызываемый метод класса, TileClass::iGetValue(). Первая часть кода функции проверяет, не пытается ли программист получить значение, которого не существует. Если из кода убрать эту проверку, программа может аварийно завершиться из-за попытки обращения к несуществующей памяти. Затем функция возвращает значение переменной класса m_iValue, относящееся к указанному слою.

Следующая функция, TileClass::vSetValue(), применяется для изменения значения указанного слоя блока. Вероятно, вы не будете использовать эту функцию слишком часто в игре, но она будет очень часто использоваться в вашей программе редактирования карт. Внутри функции производится не слишком много работы; она проверяет, что указан допустимый номер слоя, и затем устанавливает сооответствующее слою значение в члене данных класса m_iValue.

Следующие две функции, TileClass::vSetRotation() и TileClass::vSetSize, работают точно так же, как функция установки значения слоя, за исключением того, что они изменяют переменные класса m_fRotX и m_fSize.

Последние две функции, TileClass::fGetRot() и TileClass::fGetSize(), работают аналогично функции получения значения блока, за исключением того, что они возвращают значения членов данных класса m_fRotX и m_fSize.

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

Пример использования класса

Ниже приводится пример использования разработанного класса блочной графики для создания карты игрового мира:

void main()
{
   int       iMapWidth  = 10;
   int       iMapHeight = 10;
   TileClass *Tiles;
   int       iBMPToRender;

   // Выделяем память для блоков
   Tiles = new TileClass[(iMapWidth * iMapHeight)];

   //
   // Цикл выполняет перебор всех блоков
   // и нинциализирует каждый из них
   //
   for(int i = 0; i < (iMapWidth * iMapHeight); i++) {
      // Выделяем для каждого блока один слой
      Tiles[i].vSetNumLayers(1);
      // Присваиваем каждому блоку значение 0
      Tiles[i].vSetValue(0, 0);
      // Устанавливаем размер блока равным 64 пикселам
      Tiles[i].vSetSize(64, 0);
   }

   //
   // Отображение блоков с использованием
   // фиктивной функции визуализации
   //
   // Отображение горизонтальных рядов
   for(int y = 0; y < iMapHeight; y++) {
      // Отображение блоков в каждом ряду
      for(int x = 0; x < iMapWidth; x++) {
         // Отображение конкретного блока
         iBMPToRender = Tiles[x + (y * iMapWidth)].iGetValue(0);
         vRenderTile(x, y, iBMPToRender);
      }
   }
}

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

В следующем фрагменте кода в цикле осуществляется перебор и инициализация всех только что созданных объектов TileClass. Сперва в коде количество слоев устанавливается равным 1. Это позволяет задавать для каждого блока одно значение. Затем, значение каждого блока устанавливается равным 0. Последняя часть кода устанавливает размер каждого блока, равным 64 единицам.

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

Если хотите, перед продолжением чтения поиграйтесь немного с приведенным кодом. Лично я, перед тем как перейти к следующей теме собираюсь поиграть в America's Army. Если хотите, зарегистрируйтесь и попробуйте найти меня; имя моего игрока — LostLogic.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz