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

Использование триггеров карты

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

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

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

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

Сферические триггеры

Вы определяете сферический триггер (рис. 13.1) набором координат и радиусом. У сферического триггера есть два уникальных преимущества:


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

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


Кубический триггер

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


Рис. 13.2. Кубический триггер определяется координатами противоположных углов

Рис. 13.2. Кубический триггер определяется координатами противоположных углов


Цилиндрический триггер

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


Рис. 13.3. Цилиндрический триггер определяется координатами центра нижней грани, радиусом и высотой

Рис. 13.3. Цилиндрический триггер определяется координатами центра нижней грани, радиусом и высотой


Треугольный триггер

Треугольный триггер подобен полигону в том, что оба определяются тремя точками; однако в треугольном триггере три точки определяются только их координатами X и Z. Это делает треугольник двухмерным. Чтобы треугольник работал в трехмерном мире, вы должны назначить единую координату Y для размещения всех трех точек треугольника, а также высоту, на которую треугольная область распространяется вверх. Можно думать о треугольном триггере, как о трехстороннем ящике, что показано на рис. 13.4.


Рис. 13.4. Треугольные триггеры являются наиболее универсальной формой

Рис. 13.4. Использующие плоский двухмерный треугольник, простирающийся вверх от своего местоположения в мире, треугольные триггеры являются наиболее универсальной формой, которую можно использовать для триггеров


Срабатывание триггеров

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

ПРИМЕЧАНИЕ
Вы увидите, как выполняется каждая проверка столкновений в последующем разделе «Создание класса триггера».

Что делать, когда вы обнаружите, что триггер сработал? Поскольку каждому триггеру назначен идентификационный номер, вы можете использовать этот номер, чтобы определить, какое действие выполнять. Вы можете исполнить соответствующий скрипт или выполнить другую жестко закодированную функцию. Фактически, в последующем разделе «Использование триггеров» вы увидите, как с пользой применять триггеры.

Создание класса триггера

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

Класс cTrigger может загружать и сохранять файл триггеров, что упрощает редактирование списка триггеров. Этот файл является текстовым и его просто читать и редактировать. Для каждого триггера на карте используется отдельная строка текста, записанная в следующем порядке: идентификационный номер, тип триггера (0 — сферический, 1 — кубический, 2 — цилиндрический, 3 — треугольный) и состояние триггера по умолчанию (будет ли триггер взведен после загрузки). Значение 0 означает, что триггер отключен, а значение 1 означает, что триггер включен.

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

ID 0 ENABLED X Y Z RADIUS

Для параллелепипеда необходимы координаты противоположных углов:

ID 1 ENABLED X1 Y1 Z1 X2 Y2 Z2

Цилиндр вы определяете, задавая координаты центра нижней грани, плюс радиус и высоту:

ID 2 ENABLED X Y Z RADIUS HEIGHT

И, наконец, треугольник вы определяете по координатам X и Z трех углов, упорядочивая их по часовой стрелке, когда на треугольник смотрят по оси Y (точно так же, как треугольные грани определялись в главе 2, «Рисование с DirectX Graphics»). Завершают определение триггера общая координата Y всех трех точек и его высота:

ID 3 ENABLED X1 Z1 X2 Z2 X3 Z3 Y HEIGHT

Через минуту я вернусь к файлу данных триггеров. А сейчас взгляните на определение класса триггера. Класс (его заголовок объявлен в файле Trigger.h, а полный исходный код класса находится в файле Trigger.cpp) начинается с перечисления, в котором определены типы форм триггеров, которые вы можете использовать:

// Перечисление типов триггеров
enum TriggerTypes { Trigger_Sphere = 0, Trigger_Box,
                    Trigger_Cylinder, Trigger_Triangle };

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

typedef struct sTrigger {
    long Type;        // Сфера, куб и т.д.
    long ID;          // ID триггера
    BOOL Enabled;     // Флаг активности

    float x1, y1, z1; // Координаты 1
    float x2, y2, z2; // Координаты 2
    float x3, z3;     // Координаты 3
    float Radius;     // Радиус границ

    sTrigger *Prev, *Next; // Связанный список триггеров

    sTrigger() { Prev = Next = NULL; }
    ~sTrigger() { delete Next; Next = NULL }
} sTrigger;

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

Для работы со структурой sTrigger вы используете класс триггера, который управляет связанным списком триггеров и позволяет вам сохранять и загружать список этих триггеров. Взгляните на объявление класса триггера:

class cTrigger
{
  private:
    long m_NumTriggers; // Кол-во триггеров в связанном списке
    sTrigger *m_TriggerParent; // Родитель связанного списка

    long GetNextLong(FILE *fp);   // Получаем следующее long
    float GetNextFloat(FILE *fp); // Получаем следующее float

    // Функция, добавляющая триггер к связанному списку
    sTrigger *AddTrigger(long Type, long ID, BOOL Enabled);

  public:
    cTrigger();
    ~cTrigger();

    // Функции для загрузки/сохранения списка триггеров
    BOOL Load(char *Filename);
    BOOL Save(char *Filename);

    // Функции для добавления определенного триггера к списку
    BOOL AddSphere(long ID, BOOL Enabled,
                   float XPos, float YPos, float ZPos,
                   float Radius);

    BOOL AddBox(long ID, BOOL Enabled,
                float XMin, float YMin, float ZMin,
                float XMax, float YMax, float ZMax);

    BOOL AddCylinder(long ID, BOOL Enabled,
                     float XPos, float YPos, float ZPos,
                     float Radius, float Height);

    BOOL AddTriangle(long ID, BOOL Enabled,
                     float x1, float z1,
                     float x2, float z2,
                     float x3, float z3,
                     float YPos, float Height);

    // Удаление триггера с указанным ID
    BOOL Remove(long ID);

    // Освобождение всех триггеров
    BOOL Free();

    // Поиск первого триггера в заданном месте
    // (возвращает 0 если нет)
    long GetTrigger(float XPos, float YPos, float ZPos);

    // Получение состояния триггера с указанным ID
    BOOL GetEnableState(long ID);

    // Включение/отключение триггера с указанным ID
    BOOL Enable(long ID, BOOL Enable);

    // Возвращает количество триггеров
    // и родителя связанного списка
    long GetNumTriggers();
    sTrigger *GetParentTrigger();
};

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

cTrigger::cTrigger и cTrigger::~cTrigger

Как и каждый класс С++, cTrigger имеет конструктор и деструктор, которые инициализируют и освобождают содержащиеся внутри класса данные. Единственными данными, которые отслеживаются классом триггера и не содержатся в связанном списке, являются текущее количество триггеров в связанном списке и указатель на этот связанный список. Конструктор и деструктор гарантируют, что класс подготовит для использования эти две переменные и данные класса будут освобождены при удалении объекта (вызовом функции Free), что показано ниже:

cTrigger::cTrigger() {
    m_NumTriggers = 0;
    m_TriggerParent = NULL;
}

cTrigger::~cTrigger()
{
    Free();
}

cTrigger::Load и cTrigger::Save

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

Функция Load открывает текстовый файл и читает строки текста, определяющие тип, идентификатор, местоположение и специальные свойства каждого триггера (как было описано в предыдущем разделе «Создание класса триггера»). Когда достигнут конец файла, функция Load возвращает управление. Взгляните на код функции Load, чтобы увидеть, о чем я говорю:

BOOL cTrigger::Load(char *Filename)
{
    FILE *fp;
    long Type, ID;
    BOOL Enabled;
    float x1, y1, z1, x2, y2, z2, x3, z3, Radius;

    Free(); // Удаляем все текущие триггеры

    // Открываем файл
    if((fp=fopen(Filename, "rb")) == NULL)
        return FALSE;

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

    // Начинаем чтение, пока не достигнем EOF
    while(1) {
        // Получаем ID триггера
        if((ID = GetNextLong(fp)) == -1)
            break;

        Type = GetNextLong(fp); // Получаем тип

        // Получаем состояние включения
        Enabled = (GetNextLong(fp)) ? TRUE : FALSE;

        // Дальше читаем в зависимости от типа
        switch(Type) {
            case Trigger_Sphere: // Загрузка сферы
                x1 = GetNextFloat(fp); y1 = GetNextFloat(fp);
                z1 = GetNextFloat(fp); Radius = GetNextFloat(fp);
                AddSphere(ID, Enabled, x1, y1, z1, Radius);
                break;
            case Trigger_Box: // Загрузка куба
                x1 = GetNextFloat(fp); y1 = GetNextFloat(fp);
                z1 = GetNextFloat(fp); x2 = GetNextFloat(fp);
                y2 = GetNextFloat(fp); z2 = GetNextFloat(fp);
                AddBox(ID, Enabled, x1, y1, z1, x2, y2, z2);
                break;
            case Trigger_Cylinder: // Загрузка цилиндра
                x1 = GetNextFloat(fp); y1 = GetNextFloat(fp);
                z1 = GetNextFloat(fp); Radius = GetNextFloat(fp);
                y2 = GetNextFloat(fp);
                AddCylinder(ID, Enabled, x1, y1, z1, Radius, y2);
                break;
            case Trigger_Triangle: // Загрузка треугольника
                x1 = GetNextFloat(fp); z1 = GetNextFloat(fp);
                x2 = GetNextFloat(fp); z2 = GetNextFloat(fp);
                x3 = GetNextFloat(fp); z3 = GetNextFloat(fp);
                y1 = GetNextFloat(fp); y2 = GetNextFloat(fp);
                AddTriangle(ID, Enabled, x1, z1, x2, z2, x3, z3, y1, y2);
                break;
            default: fclose(fp); // Произошла какая-то ошибка
                Free();
                return FALSE;
        }
    }

    // Закрываем файл и возвращаем результаты
    fclose(fp);

    return (m_NumTriggers) ? TRUE : FALSE;
}

После чтения идентификационного номера, типа и флага активности каждого триггера, отдельный кодовый блок switch...case заботится о загрузке данных триггеров каждого типа. После чтения данных триггера вызывается отдельная функция (зависящая от типа триггера) для вставки триггера в связанный список. Эти функции AddSphere, AddBox, AddCylinder и AddTriangle.

Оставив позади функцию Load, вы видите функцию Save, которая сканирует связанный список триггеров и записывает данные каждого триггера в файл, используя тот же самый формат для каждой описывающей триггер строки текста. Взгляните:

BOOL cTrigger::Save(char *Filename)
{
    FILE *fp;
    sTrigger *TriggerPtr;

    // Контроль ошибок
    if(!m_NumTriggers)
        return FALSE;
    if((TriggerPtr = m_TriggerParent) == NULL)
        return FALSE;

    // Открываем файл
    if((fp=fopen(Filename, "wb"))== NULL)
        return FALSE;

    // Записываем все триггеры из связанного списка
    while(TriggerPtr != NULL) {
        // Записываем ID, тип и флаг активности
        fprintf(fp, "%lu ", TriggerPtr->ID);
        fprintf(fp, "%lu ", TriggerPtr->Type);
        fprintf(fp, "%lu ", (TriggerPtr->Enabled) ? 1 : 0);

        // Записываем оставшиеся данные в зависимости от типа
        switch(TriggerPtr->Type) {
            case Trigger_Sphere: // Записываем сферу
                fprintf(fp, "%lf %lf %lf %lf\r\n",
                        TriggerPtr->x1, TriggerPtr->y1, TriggerPtr->z1,
                        TriggerPtr->Radius);
                break;
            case Trigger_Box: // Записываем куб
                fprintf(fp, "%lf %lf %lf %lf %lf %lf\r\n",
                        TriggerPtr->x1, TriggerPtr->y1, TriggerPtr->z1,
                        TriggerPtr->x2, TriggerPtr->y2, TriggerPtr->z2);
                break;
            case Trigger_Cylinder: // Записываем цилиндр
                fprintf(fp, "%lf %lf %lf %lf %lf\r\n",
                        TriggerPtr->x1, TriggerPtr->y1, TriggerPtr->z1,
                        TriggerPtr->Radius, TriggerPtr->y2);
                break;
            case Trigger_Triangle: // Записываем треугольник
                fprintf(fp, "%lf %lf %lf %lf %lf %lf %lf %lf\r\n",
                        TriggerPtr->x1, TriggerPtr->z1,
                        TriggerPtr->x2, TriggerPtr->z2,
                        TriggerPtr->x3, TriggerPtr->z3,
                        TriggerPtr->y1, TriggerPtr->y2);
                break;
        }
    }

    // Закрываем файл и сообщаем об успехе
    fclose(fp);

    return TRUE;
}

cTrigger::AddTrigger

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

Держа в уме, что функция AddTrigger только выделяет память под структуру sTrigger и заполняет ее минимальным набором упомянутых данных, взгляните на код:

sTrigger *cTrigger::AddTrigger(long Type, long ID, BOOL Enabled)
{
    // Выделяем новую структуру триггера и включаем ее в список
    sTrigger *Trigger = new sTrigger();

    Trigger->Prev = NULL;
    if((Trigger->Next = m_TriggerParent) != NULL)
        m_TriggerParent->Prev = Trigger;
    m_TriggerParent = Trigger;

    // Устанавливаем тип, ID и флаг активности триггера
    Trigger->Type = Type;
    Trigger->ID = ID;
    Trigger->Enabled = Enabled;

    m_NumTriggers++; // Увеличиваем счетчик триггеров

    return Trigger; // Возвращаем указатель на структуру
}

cTrigger::AddSphere, cTrigger::AddBox, cTrigger::AddCylinder и cTrigger::AddTriangle

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

Давайте начнем с функции AddSphere, которая получает, помимо идентификационного номера триггера и состояния активности (так здесь поступает каждая из четырех функций), еще радиус сферы и координаты x, y и z ее центра:

BOOL cTrigger::AddSphere(long ID, BOOL Enabled,
                         float XPos, float YPos, float ZPos,
                         float Radius)
{
    // Создаем новую структуру триггера и включаем ее в список
    sTrigger *Trigger = AddTrigger(Trigger_Sphere, ID, Enabled);

    // Устанавливаем данные триггера
    Trigger->x1 = XPos;
    Trigger->y1 = YPos;
    Trigger->z1 = ZPos;
    Trigger->Radius = Radius * Radius;

    return TRUE;
}

 

ПРИМЕЧАНИЕ
Все функции, получающие в качестве аргумента радиус сохраняют в структуре квадрат указанного значения. В дальнейшем это ускорит проверку дистанции. Как класс триггера ускоряет проверку дистанции? Стандартная проверка дистанции использует вызов sqrt для вычисления правильного расстояния. Отказ от sqrt ускоряет работу движка, но в этом случае вы должны использовать квадраты значений для сопоставления дистанции.

Говоря кратко и по сути, функция AddSphere вызывает функцию AddTrigger для выделения памяти под структуру sTrigger и включения ее в связанный список. После создания экземпляр структуры sTrigger заполняется координатами и радиусом сферического триггера.

AddBox, AddCylinder и AddTriangle действуют тем же образом, что и функция AddSphere. Функция AddBox получает идентификационный номер и состояние активности, а также координаты двух противоположных углов параллелепипеда:

BOOL cTrigger::AddBox(long ID, BOOL Enabled,
                      float XMin, float YMin, float ZMin,
                      float XMax, float YMax, float ZMax)
{
    // Создаем новую структуру триггера и включаем ее в список
    sTrigger *Trigger = AddTrigger(Trigger_Box, ID, Enabled);

    // Устанавливаем данные триггера
    // (упорядочивая минимальные и максимальные значения)
    Trigger->x1 = min(XMin, XMax);
    Trigger->y1 = min(YMin, YMax);
    Trigger->z1 = min(ZMin, ZMax);
    Trigger->x2 = max(XMin, XMax);
    Trigger->y2 = max(YMin, YMax);
    Trigger->z2 = max(ZMin, ZMax);

    return TRUE;
}

Функция AddCylinder использует для триггера координаты центра нижней грани цилиндра, радиус и высоту. Взгляните на код AddCylinder:

BOOL cTrigger::AddCylinder(long ID, BOOL Enabled,
                           float XPos, float YPos, float ZPos,
                           float Radius, float Height)
{
    // Создаем новую структуру триггера и включаем ее в список
    sTrigger *Trigger = AddTrigger(Trigger_Cylinder, ID, Enabled);

    // Устанавливаем данные триггера
    Trigger->x1 = XPos;
    Trigger->y1 = YPos;
    Trigger->z1 = ZPos;
    Trigger->Radius = Radius * Radius;
    Trigger->y2 = Height;

    return TRUE;
}

Завершает набор AddTriangle, которая получает три пары координат X и Z, определяющих каждый из трех углов треугольника. Координата Y является общей для всех трех углов, также как и следующая за ней высота фигуры треугольного триггера. Теперь следующий фрагмент кода должен быть ясен:

BOOL cTrigger::AddTriangle(long ID, BOOL Enabled,
                           float x1, float z1,
                           float x2, float z2,
                           float x3, float z3,
                           float YPos, float Height)
{
    // Создаем новую структуру триггера и включаем ее в список
    sTrigger *Trigger = AddTrigger(Trigger_Triangle, ID, Enabled);

    // Устанавливаем данные триггера
    Trigger->x1 = x1;
    Trigger->z1 = z1;
    Trigger->x2 = x2;
    Trigger->z2 = z2;
    Trigger->x3 = x3;
    Trigger->z3 = z3;
    Trigger->y1 = YPos;
    Trigger->y2 = Height;

    return TRUE;
}

cTrigger::Remove и cTrigger::Free

Эти две функции удаляют триггеры из связанного списка, позволяя указать идентификационный номер удаляемого триггера в функции Remote, или позволяя классу удалить все триггеры из списка, используя функцию Free.

Функция Remove сканирует весь связанный список и для каждого триггера, идентификационный номер которого совпадает с тем, который передан функции Remove в аргументе ID, функция Remove удаляет структуру из связанного списка и освобождает занятую структурой память:

BOOL cTrigger::Remove(long ID)
{
    sTrigger *TriggerPtr, *NextTrigger;
    long Count = 0;

    // Сканируем список триггеров
    if((TriggerPtr = m_TriggerParent) != NULL) {
        while(TriggerPtr != NULL) {

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

            // Запоминаем следующий элемент
            NextTrigger = TriggerPtr->Next;

            // Совпадает?
            if(TriggerPtr->ID == ID) {

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

                // Удаляем из списка
                if(TriggerPtr->Prev != NULL)
                    TriggerPtr->Prev->Next = TriggerPtr->Next;
                else
                    m_TriggerParent = TriggerPtr->Next;
                if(TriggerPtr->Next != NULL)
                    TriggerPtr->Next->Prev = TriggerPtr->Prev;
                if(TriggerPtr->Prev == NULL && TriggerPtr->Next == NULL)
                    m_TriggerParent = NULL;

                // Освобождаем память
                TriggerPtr->Prev = TriggerPtr->Next = NULL;
                delete TriggerPtr;

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

                // Уменьшаем количество триггеров
                // и увеличиваем счетчик удаленных
                m_NumTriggers--;
                Count++;
            }

            // Переходим к следующему триггеру
            TriggerPtr = NextTrigger;
        }
    }

    // Возвращаем TRUE если что-то нашли и удалили
    return (Count) ? TRUE : FALSE;
}

В то время, как функция Remove удаляет триггеры согласно их идентификационным номерам, функция Free может пропустить всю эту суету и одним махом удаляет весь связанный список, используя следующий код:

BOOL cTrigger::Free()
{
    delete m_TriggerParent;
    m_TriggerParent = NULL;
    m_NumTriggers = 0;

    return TRUE;
}

cTrigger::GetTrigger

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

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

ВНИМАНИЕ!
Никогда не назначайте триггеру нулевой идентификатор, поскольку класс триггера использует ноль, чтобы сообщать при вызове функции GetTrigger о том, что никакой триггер не найден.

long cTrigger::GetTrigger(float XPos, float YPos, float ZPos)
{
    float XDiff, YDiff, ZDiff, Dist;
    D3DXVECTOR2 vecNorm;
    sTrigger *Trigger;

    // Сканируем список триггеров
    if((Trigger = m_TriggerParent) != NULL) {
        while(Trigger != NULL) {
            // Проверяем только активные
            if(Trigger->Enabled == TRUE) {

Теперь вы проверяете активный триггер, чтобы увидеть, пересекается ли он с координатами, переданными в аргументах XPos, YPos и ZPos функции GetTrigger. У каждого триггера есть специальный способ определения того, находятся ли указанные координаты внутри пространства триггера, и, используя инструкцию switch, следующий код выбирает способ выполнения проверки пересечения:

                // Проверка в зависимости от типа
                switch(Trigger->Type) {
                  case Trigger_Sphere:

Для сферы вы применяете проверку расстояния. Если расстояние до точки с заданными координатами меньше или равно радиусу сферы, персонаж коснулся триггера:

                    // Проверка расстояния для сферы
                    // (используется радиус)
                    XDiff = (float)fabs(Trigger->x1 - XPos);
                    YDiff = (float)fabs(Trigger->y1 - YPos);
                    ZDiff = (float)fabs(Trigger->z1 - ZPos);
                    Dist = XDiff*XDiff + YDiff*YDiff + ZDiff*ZDiff;

                    if(Dist <= Trigger->Radius)
                        return Trigger->ID;
                    break;
                  case Trigger_Box:

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

                    // Проверка нажождения внутри параллелепипеда
                    if(XPos >= Trigger->x1 && XPos <= Trigger->x2) {
                        if(YPos >= Trigger->y1 && YPos <= Trigger->y2) {
                            if(ZPos >= Trigger->z1 && ZPos <= Trigger->z2)
                                return Trigger->ID;
                        }
                    }
                    break;
                  case Trigger_Cylinder:

Цилиндрический триггер использует смесь сферы и ограничивающего параллелепипеда:

                    // Сначала проверяем ограничение высоты
                    if(YPos >= Trigger->y1 &&
                       YPos <= Trigger->y1 + Trigger->y2) {
                        // Проверка расстояния от цилиндра
                        XDiff = (float)fabs(Trigger->x1 - XPos);
                        YDiff = (float)fabs(Trigger->y1 - YPos);
                        ZDiff = (float)fabs(Trigger->z1 - ZPos);

                        Dist = XDiff*XDiff + YDiff*YDiff + ZDiff*ZDiff;

                        if(Dist <= Trigger->Radius)
                            return Trigger->ID;
                    }
                    break;
                  case Trigger_Triangle:

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

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

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

                    // Сперва проверяем ограничение высоты
                    if(YPos >= Trigger->y1 &&
                       YPos <= Trigger->y1 + Trigger->y2) {

                        // Проверяем, что точка перед всеми линиями
                        // от x1,z1 к x2,z2
                        D3DXVec2Normalize(&vecNorm,
                             &D3DXVECTOR2(Trigger->z2 - Trigger->z1,
                                          Trigger->x1 - Trigger->x2));
                        if(D3DXVec2Dot(&D3DXVECTOR2(XPos-Trigger->x1,
                                                    ZPos-Trigger->z1),
                           &vecNorm) < 0)
                            break;

                        // от x2,z2 к x3,z3
                        D3DXVec2Normalize(&vecNorm,
                             &D3DXVECTOR2(Trigger->z3 - Trigger->z2,
                                          Trigger->x2 - Trigger->x3));
                        if(D3DXVec2Dot(&D3DXVECTOR2(XPos-Trigger->x2,
                                                    ZPos-Trigger->z2),
                           &vecNorm) < 0)
                            break;

                        // от x3,z3 к x1,z1
                        D3DXVec2Normalize(&vecNorm,
                             &D3DXVECTOR2(Trigger->z1 - Trigger->z3,
                                          Trigger->x3 - Trigger->x1));
                        if(D3DXVec2Dot(&D3DXVECTOR2(XPos-Trigger->x3,
                                                    ZPos-Trigger->z3),
                           &vecNorm) < 0)
                            break;

                        return Trigger->ID;
                    }
                    break;
                }
            }

            // Переходим к следующему триггеру
            Trigger = Trigger->Next;
        }
    }
    return 0; // Триггеры не найдены
}

cTrigger::GetEnableState и cTrigger::Enable

Функция GetEnableState проверяет текущее состояние триггера; вы передаете идентификационный номер триггера и получаете возвращенное состояние триггера. Если триггер отключен, вызов GetEnableState вернет FALSE. Если включен, вернет TRUE. Чтобы включить или выключить триггер, вызовите функцию Enable, используя в качестве первого аргумента идентификационный номер триггера.

Каждая из этих двух функций сканирует связанный список структур sTrigger. Функция GetEnableState возвращает значение флага активности первой найденной структуры из списка у которой идентификационный номер совпадает со значением, предоставленным в аргументе ID.

В функции Enable связанный список сканируется, и каждый экземпляр структуры, где идентификационный номер совпадает с переданным в аргументе ID, флаг активности устанавливается в значение, переданное в аргументе Enable. Посмотрите на код этих функций:

BOOL cTrigger::GetEnableState(long ID)
{
    sTrigger *TriggerPtr;

    // Перебираем все триггеры, глядя на ID
    if((TriggerPtr = m_TriggerParent) != NULL) {
        while(TriggerPtr != NULL) {

            // Если ID совпадает, возвращаем состояние
            if(TriggerPtr->ID == ID)
                return TriggerPtr->Enabled;

            // Переходим к следующему триггеру
            TriggerPtr = TriggerPtr->Next;
        }
    }

    // Возвращаем FALSE если ничего не нашли
    return FALSE;
}

BOOL cTrigger::Enable(long ID, BOOL Enable)
{
    sTrigger *TriggerPtr;
    long Count = 0;

    // Перебираем все триггеры, глядя на ID
    if((TriggerPtr = m_TriggerParent) != NULL) {
        while(TriggerPtr != NULL) {

            // Если ID совпадает, устанавливаем флаг
            // и увеличиваем счетчик
            if(TriggerPtr->ID = = ID) {
                TriggerPtr->Enabled = Enable;
                Count++;
            }

            // Переходим к следующему триггеру
            TriggerPtr = TriggerPtr->Next;
        }
    }

    // Возвращаем TRUE если какой-нибудь триггер изменен
    return (Count) ? TRUE : FALSE;
}

cTrigger::GetNumTriggers и cTrigger::GetParentTrigger

Как я люблю делать во всех моих классах, здесь я включаю две функции, которые вы можете использовать чтобы получить количество структур sTrigger в связанном списке, а также указатель на первую структуру (корневую, или родительскую, структуру) помещенную в список. Вы программируете эти две функции, GetNumTriggers и GetParentTrigger, следующим образом:

long cTrigger::GetNumTriggers()
{
    return m_NumTriggers;
}

sTrigger *cTrigger::GetParentTrigger()
{
    return m_TriggerParent;
}

Использование триггеров

Как я обещал ранее, мы вновь возвращаемся к использованию файлов для хранения триггеров карты, на этот раз воспользовавшись классом cTrigger, созданным в разделе «Создание класса триггера». В этом разделе вы увидите как эффективно определять и загружать файл триггеров.

ПРИМЕЧАНИЕ
Пример Mapping, находящийся на прилагаемом к книге CD-ROM (посмотрите в \BookCode\Chap13\Mapping) демонстрирует использование триггеров гораздо лучше, чем приведенный здесь небольшой пример. Не забудьте исследовать его!

Определение файла триггеров

Начнем с примера файла данных триггеров (с именем test.trg):

1 0 1 -900  0 900  620
2 1 0    0  0   0  100 100 100
3 2 1  100 10 200   20 100
4 3 0   10 10  10 -100 -50   0 0 100

Первый триггер (ID# 1) — это сфера, расположенная в –900, 0, 900 с радиусом 620. Второй триггер (ID# 2) — это куб, охватывающий область от 0, 0, 0 до 100, 100, 100. Третий триггер (ID# 3) является цилиндром, центр нижней грани которого находится в точке 100, 10, 200, с радиусом 20 и высотой 100 единиц. Четвертый триггер (ID# 4) — треугольник, охватывающий область от 10, 10 до 10, –100 и до –50, 0; координата Y (низ треугольной призмы) равна 0, и простирается он на 100 единиц вверх. Обратите внимание, что все остальные триггеры по умолчанию отключены.

Загрузка файла триггеров

Чтобы загрузить файл триггеров, создайте экземпляр cTrigger и вызовите Load:

cTrigger Trigger;
Trigger.Load("test.trg");

Касание триггера

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

long TriggerID;

TriggerID = Trigger.GetTrigger(CharXPos, CharYPos, CharZPos);

if(TriggerID)
    MessageBox(NULL, "Trigger touched!", "Message", MB_OK);

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


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

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