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

Разработка боевых последовательностей

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

Чтобы увидеть, как я разрабатывал движок боевых последовательностей, скопируйте демонстрационный проект Battle с CD-ROM и изучайте его вместе с моими описаниями каждой функции из файла WinMain.cpp. Эти функции сравнительно небольшие, поскольку большую часть работы выполняют контроллер персонажей и контроллер заклинаний. Ваш боевой движок добавляет персонажи к драке, используя функцию Add контроллера персонажей, собирает и обрабатывает действия игрока и соответствующим образом обновляет персонажи.

Весь проект содержится внутри объекта класса cApplication из системного ядра. Вот объявление класса cApplication (наследуемого как cApp):

class cApp : public cApplication
{
    friend class cChars;

  private:
    cGraphics     m_Graphics;      // Объект cGraphics
    cCamera       m_Camera;        // Объект cCamera
    cFont         m_Font;          // Объект cFont
    cWindow       m_Stats;         // Окно для состояния  HP/MP
    cWindow       m_Options;       // Окно для заклинаний
    cInput        m_Input;         // Объект cInput 
    cInputDevice  m_Keyboard;      // Объект ввода с клавиатуры
    cInputDevice  m_Mouse;         // Объект ввода от мыши
    cMesh         m_TerrainMesh;   // Сетка ландшафта
    cObject       m_TerrainObject; // Объект ландшафта
    cVertexBuffer m_Target;        // Объект цели
    cTexture      m_Buttons;       // Кнопки и прочие рисунки

    // Контроллеры персонажей и заклинаний
    cCharacterController m_CharController;
    cSpellController m_SpellController;

    sItem m_MIL[1024]; // Главный спсиок предметов

    // Смотрим, на какой персонаж указывает мышь
    long GetCharacterAt(long XPos, long YPos);

  public:
    cApp();
    BOOL Init();
    BOOL Shutdown();
    BOOL Frame();
};

Класс приложения (cApp) выглядит довольно маленьким для такого амбициозного проекта, как боевые последовательности, но не позволяйте его размеру одурачить вас. Это мощный проект! Помните, что ранее разработанные контроллеры персонажей и заклинаний делают большую часть работы. Контроллеры персонажей и заклинаний идентичны тем, что вы видели в главе 12; даже код инициализации объектов тот же самый.

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

В проекте Battle я сконструировал простую, небольшую сетку для арены (рис. 14.4) и загрузил ее в объекты m_TerrainMesh и m_TerrainObject. Вам не требуется дерево узлов, поскольку обычно сетка уровня точно подходит под размеры экрана. Сетки уровней боевых последовательностей не должны быть большими, так что проектируя свои собственные уровни, сохраняйте сетки достаточно небольшими, чтобы они помещались на экране.


Рис. 14.4. Сетка арены для проекта Battle

Рис. 14.4. Сетка арены для проекта Battle


Управление прямолинейно. Чтобы ваш персонаж атаковал или произнес заклинание, вы должны сперва выбрать цель, на которую будет направлена атака или заклинание. Выбор цели осуществляется щелчком левой кнопки мыши по любому участвующему в битве персонажу. На экране возле выбранного персонажа появятся два маленьких вращающихся треугольника, отмечающие цель (они хранятся в буфере вершин m_Target). Игрок может в любое время выбрать любой персонаж.

Когда у игрока полный заряд (заряд отображается медленно растущей полоской в нижнем правом углу экрана), он может выбрать тип выполняемого действия. Щелчок Attack заставит игрока ударить выбранную цель (нанося ей таким образом повреждения). Щелчок Spell открывает список известных заклинаний; щелчок по заклинанию приводит к его произнесению для выбранной цели. Помимо добавленного механизма выбора цели здесь нет ничего действительно нового. Итак, почему бы теперь не просмотреть код приложения?

Глобальные данные

Класс приложения использует три глобальные переменные для хранения данных о сетках персонажей и заклинаний и информации об анимации персонажей:

#include "Core_Global.h"
#include "Window.h"
#include "Chars.h"
#include "WinMain.h"

// Глобальные имена сеток персонажей
char *g_CharMeshNames[] = {
    { "..\\Data\\Warrior.x" }, // Сетка # 0
    { "..\\Data\\Yodan.x" }    // Сетка # 1
};

sCharAnimationInfo g_CharAnimations[] = {
    { "Idle", TRUE },
    { "Walk", TRUE },
    { "Swing", FALSE },
    { "Spell", FALSE },
    { "Swing", FALSE },
    { "Hurt", FALSE },
    { "Die", FALSE },
    { "Idle", TRUE }
};

char *g_SpellMeshNames[] = {
    { "..\\Data\\Fireball.x" },
    { "..\\Data\\Explosion.x" },
    { "..\\Data\\Groundball.x" },
    { "..\\Data\\ice.x" },
    { "..\\Data\\bomb.x" },
};

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

cApp::cApp

Вы используете конструктор класса приложения только для инициализации данных класса, что включает конфигурирование окна приложения:

cApp::cApp()
{
    m_Width  = 640;
    m_Height = 480;
    m_Style  = WS_BORDER|WS_CAPTION|WS_MINIMIZEBOX|WS_SYSMENU;
    strcpy(m_Class, "BattleClass");
    strcpy(m_Caption, "Battle Demo by Jim Adams");
}

cApp::Init

Init, первая перегруженная функция в классе приложения, инициализирует графическую систему и ввод, а также загружает всю графику, шрифты, предметы и другие данные, необходимые для программы:

BOOL cApp::Init()
{
    long i;
    FILE *fp;

    // Инициализация графического устройства
    // и установка разрешения экрана
    m_Graphics.Init();
    m_Graphics.SetMode(GethWnd(), TRUE, TRUE);
    m_Graphics.SetPerspective(D3DX_PI/4, 1.3333f, 1.0f, 10000.0f);
    ShowMouse(TRUE);

    // Создание шрифта
    m_Font.Create(&m_Graphics, "Arial", 16, TRUE);

Как и в обычном графическом проекте, инициализируется графическая система и создается шрифт Arial. Затем идет инициализация системы и устройств ввода:

    // Инициализация системы и устройств ввода
    m_Input.Init(GethWnd(), GethInst());
    m_Keyboard.Create(&m_Input, KEYBOARD);
    m_Mouse.Create(&m_Input, MOUSE, TRUE);

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


Рис. 14.5. В соответствии с проектом буфер вершин указателя цели вращается относительно центральной координаты (точки отсчета)

Рис. 14.5. В соответствии с проектом буфер вершин указателя цели вращается относительно центральной координаты (точки отсчета)


Сейчас функция Init создает буфер вершин, содержащий два треугольника, которые будут указывать на цель:

    // Создаем буфер вершин указателя на цель
    typedef struct {
        float x, y, z;
        D3DCOLOR Diffuse;
    } sVertex;

    sVertex Vert[6] = {
        { -20.0f,  40.0f, 0.0f, 0xFFFF4444 },
        {  20.0f,  40.0f, 0.0f, 0xFFFF4444 },
        {   0.0f,  20.0f, 0.0f, 0xFFFF4444 },
        {   0.0f, -20.0f, 0.0f, 0xFFFF4444 },
        {  20.0f, -40.0f, 0.0f, 0xFFFF4444 },
        { -20.0f, -40.0f, 0.0f, 0xFFFF4444 }
    };

    m_Target.Create(&m_Graphics, 6,
                    D3DFVF_XYZ|D3DFVF_DIFFUSE,
                    sizeof(sVertex));
    m_Target.Set(0, 6, &Vert);

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


Рис. 14.6. Изображения кнопок содержат кнопки Attack и Spell, а также полосу таймера зарядки

Рис. 14.6. Изображения кнопок содержат кнопки Attack и Spell, а также полосу таймера зарядки


Изображение, используемое для рисования таймера, зарядки объединено с графическими изображениями кнопок. Затем вы загружаете сетку и объект арены.

    // Загрузка кнопок и другой графики
    m_Buttons.Load(&m_Graphics, "..\\Data\\Buttons.bmp");

    // Загрузка сетки ландшафта и установка объекта
    m_TerrainMesh.Load(&m_Graphics, "..\\Data\\Battle.x",
                       "..\\Data\\");
    m_TerrainObject.Create(&m_Graphics, &m_TerrainMesh);

Чтобы отображать состояние игрока (включающее очки здоровья и маны, как было определено в главе 12), вы создаете текстовое окно (m_Stats — объект cWindow из главы 12) ниже персонажа. Второе окно (m_Options) создается для отображения имен всех известных заклинаний, из которых игрок может выбирать необходимое заклинание. Как видно на рис. 14.7, это второе окно располагается поверх изображения.

    // Создаем текстовые окна
    m_Stats.Create(&m_Graphics, &m_Font);
    m_Stats.Move(508, 400, 128, 48);

    m_Options.Create(&m_Graphics, &m_Font);
    m_Options.Move(4, 4, 632, 328);

Рис. 14.7. Текстовые окна позволяют приложению показывать состояние игрока и все известные заклинания

Рис. 14.7. Текстовые окна позволяют приложению показывать состояние игрока и все известные заклинания


Затем вам надо загрузить главный список предметов и инициализировать классы контроллеров персонажей и заклинаний. Код инициализации контроллеров идентичен используемому в демонстрационном проекте Chars из главы 12.

    // Загрузка главного списка предметов
    for(i = 0; i < 1024; i++)
        ZeroMemory(&m_MIL[i], sizeof(sItem));

    if((fp=fopen("..\\Data\\Default.mil", "rb")) != NULL) {
        for(i = 0; i < 1024; i++)
            fread(&m_MIL[i], 1, sizeof(sItem), fp);
        fclose(fp);
    }

    // Инициализация контроллера персонажей
    m_CharController.Init(&m_Graphics, NULL, &m_Font,
                          "..\\Data\\Default.mcl", (sItem*)&m_MIL,
                          m_SpellController.GetSpell(0),
                          sizeof(g_CharMeshNames) / sizeof(char*),
                          g_CharMeshNames,
                          "..\\Data\\", "..\\Data\\",
                          sizeof(g_CharAnimations) / sizeof(sCharAnimationInfo),
                          (sCharAnimationInfo*)&g_CharAnimations,
                          &m_SpellController);

    // Инициализация контроллера заклинаний
    m_SpellController.Init(&m_Graphics, NULL,
                           "..\\Data\\Default.msl",
                           sizeof(g_SpellMeshNames) / sizeof(char*), g_SpellMeshNames,
                           "..\\Data\\", &m_CharController);

И, завершая функцию Init, вы размещаете на арене несколько персонажей (игрока и монстров). (Здесь меня одолела лень; жесткое программирование участвующих персонажей и их местоположения следует заменить на случайный выбор, но я оставлю это для вас.) Чтобы добавить персонаж к сражению и разместить его, используйте функцию Add контроллера персонажей, как показано ниже:

    // Добавим персонаж игрока
    m_CharController.Add(0, 0, CHAR_PC, CHAR_STAND,
                         200.0f, 0.0f, 0.0f, 4.71f);

    // Все остальные персонажи жестко запрограммированы
    m_CharController.Add(1, 1, CHAR_MONSTER, CHAR_STAND,
                         -200.0f, 0.0f, 0.0f, 1.57f);
    m_CharController.Add(2, 1, CHAR_MONSTER, CHAR_STAND,
                         -100.0f, 0.0f, -200.0f, 1.57f);
    m_CharController.Add(3, 1, CHAR_MONSTER, CHAR_STAND,
                         0.0f, 0.0f, 100.0f, 1.57f);

    // Дадим одному из монстров топор
    m_CharController.Equip(m_CharController.GetCharacter(1), \
                           8, WEAPON, TRUE);

    return TRUE;
}

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

cApp::Shutdown

Функция cApp::Shutdown (являющаяся противоположностью cApp::Init) освобождает все используемые ресурсы (сетки, объекты и контроллеры) как показано ниже:

BOOL cApp::Shutdown()
{
    // Освобождаем контроллеры
    m_CharController.Free();
    m_SpellController.Free();

    // Освобождаем объекты и сетки
    m_TerrainMesh.Free();
    m_TerrainObject.Free();

    // Освобождаем окна
    m_Stats.Free();
    m_Options.Free();

    // Освобождаем буфер вершин указателя цели
    m_Target.Free();
    m_Buttons.Free();

    // Отключаем ввод
    m_Keyboard.Free();
    m_Mouse.Free();
    m_Input.Shutdown();

    // Отключаем графику
    m_Font.Free();
    m_Graphics.Shutdown();

    return TRUE;
}

cApp::Frame

Функция cApp::Frame в ее текущем воплощении заметно разрослась. Здесь Frame выполняет работу по сбору и обработке пользовательского ввода. Для ввода используется только мышь; левая кнопка мыши выбирает целевой персонаж, произнесение заклинания или выполнение атаки. Правая кнопка мыши закрывает окно выбора заклинания, если оно открыто. Как только выбраны цель и действие, выполняется соответствующая обработка.

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

BOOL cApp::Frame()
{
    static DWORD UpdateCounter = timeGetTime();
    static sCharacter *PCChar=m_CharController.GetCharacter(0);
    static BOOL SelectSpell = FALSE;
    static long TargetID = -1;
    cWorldPosition Pos;
    sCharacter *CharPtr;
    sSpell *SpellPtr;
    char Text[128];
    long x, y, Num, i;
    float MinY, MaxY, YOff;

    // Ограничиваем до 30 fps
    if(timeGetTime() < UpdateCounter + 33)
        return TRUE;
    UpdateCounter = timeGetTime();

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

ПРИМЕЧАНИЕ
Чтобы вычислить количество миллисекунд ожидания в вашей игре, разделите 1000 на используемое значение частоты кадров. В данном случае 1000 делим на 30 и получаем 33,333333, округляемое до 33.

    // Чтение ввода
    m_Keyboard.Acquire(TRUE);
    m_Keyboard.Read();
    m_Mouse.Acquire(TRUE);
    m_Mouse.Read();

    // Выход, если нажата ESC
    if(m_Keyboard.GetKeyState(KEY_ESC) == TRUE)
        return FALSE;

Затем Frame определяет, что делать, если игрок нажал левую кнопку мыши. Помните, что вы можете выбирать цель, произносить заклинание или атаковать:

    // Получаем выбранного персонажа, если нажата левая кнопка
    if(m_Mouse.GetButtonState(MOUSE_LBUTTON) == TRUE) {
        // Получаем координаты указателя мыши
        x = m_Mouse.GetXPos();
        y = m_Mouse.GetYPos();

        // Блокируем кнопки мыши
        m_Mouse.SetLock(MOUSE_LBUTTON, TRUE);
        m_Mouse.SetButtonState(MOUSE_LBUTTON, FALSE);

Далее Frame определяет, что обрабатывать. Если игрок щелкнул по кнопке Spell, открывается окно списка заклинаний (m_Options), показывающее все известные заклинания. Когда окно списка заклинаний открыто, флаг SelectSpell (который по умолчанию установлен в FALSE) устанавливается в TRUE и Frame ждет, пока будет выбрано заклинание или закрыто окно (по щелчку правой кнопкой мыши).

        // Смотрим, выбираем ли заклинание
        if(SelectSpell == TRUE) {
            // Получаем указатель на заклинание
            Num = ((y-8)/20) * 4 + ((x-8)/150);

Вы выбираете заклинание, вычисляя координаты щелчка мыши на экране и сравнивая их с местоположением, где в окне печатается каждое из названий заклинаний. Каждое название заклинания занимает область окна 150 × 20 пикселов (сами области начинаются с экранных координат 8, 8), что дает место, достаточное для размещения в окне 64 названий заклинаний.

            // Проверяем, знает ли игрок заклинание
            // (и достаточно ли у него маны)
            if(Num >= 0 && Num < 64) {
                SpellPtr = m_SpellController.GetSpell(Num);
                if(PCChar->Def.MagicSpells[Num/32] & (1<<(Num & 31)) &&
                              SpellPtr->Name[0] &&
                              PCChar->ManaPoints >= SpellPtr->Cost) {
                    // Устанавливаем номер произносимого заклинания
                    PCChar->SpellNum = Num; 
                    // Устанавливаем цель
                    PCChar->SpellTarget = CHAR_MONSTER; 
                    m_CharController.SetAction(PCChar, CHAR_SPELL);
                    SelectSpell = FALSE; // Закрываем окно выбора

Как только игрок выбирает заклинание в окне со списком заклинаний, соответствующее заклинание начинает действовать и класс контроллера обрабатывает его эффекты. Окно выбора заклинаний помечается как закрытое и исполнение продолжается. Если выбор заклинания еще не был начат, функция Frame продолжает выполнение, проверяя, не щелкнул ли игрок по кнопкам Attack или Spell.

                }
            }
        } else {
            // Смотрим, щелкнули ли по кнопке
            // (если выбрана цель и закончен заряд)
            if(TargetID != -1 && PCChar->Charge >= 100.0f) {
                // Устанавливаем сведения о жертве и атакующем
                CharPtr = m_CharController.GetCharacter(TargetID);
                PCChar->Victim = CharPtr;
                CharPtr->Attacker = PCChar;
                PCChar->TargetX = CharPtr->XPos;
                PCChar->TargetY = CharPtr->YPos;
                PCChar->TargetZ = CharPtr->ZPos;

                // Определяем, выбрана ли атака,
                // сравнивая координаты указателя мыши с
                // координатами кнопки Attack на экране,
                // находящимися в диапазоне 572,328 - 636, 360
                if(x >= 572 && x < 636 && y >= 328 && y < 360)
                    m_CharController.SetAction(PCChar, CHAR_ATTACK);

                // Определяем, выбрано ли произнесение заклинания,
                // сравнивая координаты указателя мыши с
                // координатами кнопки Spell на экране,
                // находящимися в диапазоне 572,364 - 636, 396
                if(x >= 572 && x < 636 && y >= 364 && y < 396)
                    SelectSpell = TRUE;
            }

После того, как действие выбрано (и если указана цель и таймер зарядки игрока достиг максимума), устанавливается информация об атаке. Если игрок щелкает по кнопке Attack, инициируются действия атаки. Если игрок щелкает по кнопке Spell, открывается окно выбора заклинания.

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

            // Смотрим, выбран ли персонаж
            TargetID = GetCharacterAt(x, y);
        }
    }

    // Очищаем состояние заклинания, если нажата правая кнопка мыши
    if(m_Mouse.GetButtonState(MOUSE_RBUTTON) == TRUE) {
        // Блокируем кнопки мыши
        m_Mouse.SetLock(MOUSE_RBUTTON, TRUE);
        m_Mouse.SetButtonState(MOUSE_RBUTTON, FALSE);
        SelectSpell = FALSE;
    }

    // Обновляем контроллеры
    m_CharController.Update(33);
    m_SpellController.Update(33);

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

    // Установка камеры
    m_Camera.Point(300.0f, 300.0f, -340.0f, 0.0f, 0.0f, 0.0f);
    m_Graphics.SetCamera(&m_Camera);

    // Визуализируем все
    m_Graphics.Clear(D3DCOLOR_RGBA(0,32,64,255));
    if(m_Graphics.BeginScene() == TRUE) {

Камера установлена, сцена очищена и начинается визуализация сцены путем рисования арены, персонажей и заклинаний.

        // Визуализация ландшафта
        m_Graphics.EnableZBuffer(TRUE);
        m_TerrainObject.Render();

        // Визуализация всех персонажей
        m_CharController.Render();

        // Визуализация заклинаний
        m_SpellController.Render();

        // Проверяем, надо ли визуализировать указатель цели
        if(TargetID != -1) {
            // Перемещаем указатель цели в
            // место нахождения персонажа
            CharPtr = m_CharController.GetCharacter(TargetID);
            Pos.EnableBillboard(TRUE);
            Pos.Move(CharPtr->XPos, CharPtr->YPos, CharPtr->ZPos);
            Pos.Rotate(0.0f, 0.0f, (float)timeGetTime() / 100.0f);

            // Смещаем на половину высоты персонажа
            CharPtr->Object.GetBounds(NULL, &MinY, NULL,
                                NULL, &MaxY, NULL, NULL);
            YOff = MinY + ((MaxY - MinY) * 0.5f);
            Pos.MoveRel(0.0f, YOff, 0.0f);

            // Визуализируем указатель цели
            m_Graphics.SetTexture(0, NULL);
            m_Graphics.EnableZBuffer(FALSE);
            m_Graphics.SetWorldPosition(&Pos);
            m_Target.Render(0, 2, D3DPT_TRIANGLELIST);
            m_Graphics.EnableZBuffer(TRUE);
        }

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

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

        // Отображение экрана состояния
        sprintf(Text, "HP: %ld / %ld\r\nMP: %ld / %ld",
                PCChar->HealthPoints, PCChar->Def.HealthPoints,
                PCChar->ManaPoints, PCChar->Def.ManaPoints);
        m_Stats.Render(Text);

Затем вы отображаете таймер зарядки. Значение таймера зарядки находится в диапазоне от 0 до 100, и используется ранее загруженное в объект m_Buttons изображение кнопок, из которого вырезается только небольшая часть изображения, представляющая текущий уровень заряда.

        // Отображаем измеритель заряда
        m_Graphics.BeginSprite();
        m_Buttons.Blit(508, 450, 0, 64, 128, 16);
        m_Buttons.Blit(510, 452, 0, 80,(long)(1.24f*PCChar->Charge), 12);
        m_Graphics.EndSprite();

Теперь рисуем кнопки действий (только если таймер заряда достиг максимального значения):

        // Отображение вариантов атаки
        if(m_CharController.GetCharacter(0)->Charge >= 100.0f) {
            m_Graphics.BeginSprite();
            m_Buttons.Blit(572, 328, 0, 0, 64, 32);
            m_Buttons.Blit(572, 364, 0, 32, 64, 32);
            m_Graphics.EndSprite();
        }

В завершение визуализации, если необходимо, рисуем окно выбора заклинаний, и отображаем каждое из известных заклинаний, чтобы игрок мог сделать свой выбор:

        // Отображаем список заклинаний
        if(SelectSpell == TRUE) {
            m_Options.Render();

            // Отображаем известные заклинания
            for(i = 0; i < 64; i++) {
                SpellPtr = m_SpellController.GetSpell(i);
                if(PCChar->Def.MagicSpells[i/32] & (1<<(i&31)) &&
                           SpellPtr->Name[0] &&
                           PCChar->ManaPoints >= SpellPtr->Cost) {
                    x = i % 4 * 150;
                    y = i / 4 * 20;
                    m_Font.Print(m_SpellController.GetSpell(i)->Name,
                                 x+8, y+8);
                }
            }
        }
        m_Graphics.EndScene();
    }
    m_Graphics.Display();

    return TRUE;
}

cApp::GetCharacterAt

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

long cApp::GetCharacterAt(long XPos, long YPos)
{
    D3DXVECTOR3 vecRay, vecDir;
    D3DXVECTOR3 vecMeshRay, vecMeshDir;
    D3DXVECTOR3 vecTemp;
    D3DXMATRIX matProj, matView, *matWorld;
    D3DXMATRIX matInv;
    DWORD FaceIndex;
    BOOL Hit;
    float u, v, Dist;
    sCharacter *CharPtr;
    sMesh *MeshPtr;

    // Получение родительского объекта персонажа
    if((CharPtr = m_CharController.GetParentCharacter()) == NULL)
        return -1;

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

    // Получаем матрицу проекции, матрицу вида
    // и инвертированную матрицу вида
    m_Graphics.GetDeviceCOM()->GetTransform(D3DTS_PROJECTION,
                                            &matProj);
    m_Graphics.GetDeviceCOM()->GetTransform(D3DTS_VIEW,
                                            &matView);
    D3DXMatrixInverse(&matInv, NULL, &matView);

    // Вычисляем вектор луча выбора в экранном пространстве
    vecTemp.x = (((2.0f * (float)XPos) /
                (float)m_Graphics.GetWidth()) - 1.0f) /
                matProj._11;
    vecTemp.y = -(((2.0f * (float)YPos) /
                (float)m_Graphics.GetHeight()) - 1.0f) /
                matProj._22;
    vecTemp.z = 1.0f;

    // Преобразуем луч в экранном пространстве
    vecRay.x = matInv._41;
    vecRay.y = matInv._42;
    vecRay.z = matInv._43;
    vecDir.x = vecTemp.x * matInv._11 +
               vecTemp.y * matInv._21 +
               vecTemp.z * matInv._31;
    vecDir.y = vecTemp.x * matInv._12 +
               vecTemp.y * matInv._22 +
               vecTemp.z * matInv._32;
    vecDir.z = vecTemp.x * matInv._13 +
               vecTemp.y * matInv._23 +
               vecTemp.z * matInv._33;

Теперь луч сконфигурирован (точно так же, как вы конфигурировали его в главе 8, «Создание трехмерного графического движка»), и вы продолжаете исполнение, входя в цикл, перебирающий всех персонажей. Для каждого персонажа функция GetCharacterAt сканирует все образующие персонаж сетки, выполняя проверку пересечения луча с полигоном.

    // Сканируем каждый персонаж и проверяем пересечение
    while(CharPtr != NULL) {
        // Сканируем сетки персонажа
        MeshPtr = CharPtr->Object.GetMesh()->GetParentMesh();

        while(MeshPtr != NULL) {
            // Преобразуем луч и направление с помощью
            // матрицы мирового преобразования объекта
            matWorld = CharPtr->Object.GetMatrix();
            D3DXMatrixInverse(&matInv, NULL, matWorld);
            D3DXVec3TransformCoord(&vecMeshRay, &vecRay, &matInv);
            D3DXVec3TransformNormal(&vecMeshDir, &vecDir, &matInv);

            // Проверяем пересечение
            D3DXIntersect(MeshPtr->m_Mesh, &vecMeshRay,&vecMeshDir, \
                          &Hit, &FaceIndex, &u, &v, &Dist);

 

ВНИМАНИЕ!
Будьте внимательны! Если ваши сетки были созданы с указанием флага, делающего их доступными только для чтения, вызов D3DXIntersect скорее всего приведет к сбою. Это происходит из-за того, что функция D3DXIntersect не может заблокировать и прочитать буфер вершин сетки. Чтобы гарантировать, что вызов GetCharacterAt будет работать, убедитесь, что при создании сеток указываете флаг, позволяющий чтение.

Обратите внимание, что для каждого рассматриваемого персонажа вы выполняете смещение координат луча в соответствии с ориентацией персонажа в мире (получаемой из матрицы преобразования объекта персонажа). Вы поступаете так потому что функция проверки пересечения не принимает во внимание мировое преобразование каждого персонажа; это делаете вы.

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

            // Проверяем, пересекает ли луч персонаж
            // и возвращаем ID, если да
            if(Hit == TRUE)
                return CharPtr->ID;

            // Переходим к следующей сетке
            MeshPtr = MeshPtr->m_Next;
        }

        // Переходим к следующему персонажу
        CharPtr = CharPtr->Next;
    }

    return -1; // Нет попаданий
}

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

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