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

Создание класса контроллера персонажей

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

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

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

Сетки и sCharacterMeshList

Ранее, в разделе «Сетки и sSpellMeshList» вы прочитали о контроллере заклинаний и том, как контроллер поддерживает список сеток. Для контроллера персонажей у вас также есть список сеток, используемых для визуализации персонажей. Структура sCharacterMeshList содержит объекты анимации и сетки и имя файла.

typedef struct sCharacterMeshList {
    char Filename[MAX_PATH]; // Имя файла сетки/анимации
    long Count;              // Кол-во использующих сетку персонажей
    cMesh Mesh;              // Объект сетки
    cAnimation Animation;    // Объект анимации

    sCharacterMeshList() { Count = 0; }
    ~sCharacterMeshList() { Mesh.Free(); Animation.Free(); }
} sCharacterMeshList;

Зацикливание анимации и sCharAnimationInfo

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

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

typedef struct {
    char Name[32]; // Имя анимации
    BOOL Loop;     // Флаг зацикливания
} sCharAnimationInfo;

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

Перемещение и sRoutePoint

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

typedef struct {
    float XPos, YPos, ZPos; // Позиция цели
} sRoutePoint;

Отслеживание персонажей с sCharacter

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

typedef struct sCharacter
{
    long Definition; // Номер определения персонажа
    long ID;         // Идентификатор персонажа

    long Type;       // PC, NPC или MONSTER
    long AI;         // STAND, WANDER и т.д.

    BOOL Enabled;    // Флаг активности (для обновлений)

Для начала каждому персонажу необходимо определение, которое берется из главного списка персонажей. Вы храните номер этого определения в переменной Definition. Чтобы различать похожих персонажей вы назначаете каждому персонажу уникальный идентификационный номер (ID). Думайте об использовании идентификаторов так же, как об использовании имен. Вместо того, чтобы в ходе игры добавить персонаж с именем Джордж, вы ссылаетесь на тот же персонаж, как на персонаж 5.

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

#define CHAR_PC      0
#define CHAR_NPC     1
#define CHAR_MONSTER 2

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

#define CHAR_STAND  0
#define CHAR_WANDER 1
#define CHAR_ROUTE  2
#define CHAR_FOLLOW 3
#define CHAR_EVADE  4

И, наконец, каждому персонажу нужно разрешение обновления. Это определяет флаг Enabled и, если он равен TRUE, контроллеру разрешено обновлять персонаж в каждом кадре, в то время как значение флага FALSE означает, что персонаж никогда не обновляется (пока вы не разрешите).

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

    sCharacterDefinition Def;      // Загруженное определение
    cCharICS *CharICS;             // ICS персонажа

    char ScriptFilename[MAX_PATH]; // Назначенный скрипт

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

    long  HealthPoints; // Текущие очки здоровья
    long  ManaPoints;   // Текущие очки маны
    long  Ailments;     // Дополнительные статусы персонажа
    float Charge;       // Заряд атаки

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

ПРИМЕЧАНИЕ
Помните, что отдельные действия (например, атаку или произнесение заклинания) персонажи могут выполнять только когда их заряд достигнет максимального значения. Этот заряд увеличивается со скоростью, заданной в MCL.

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

    long  Action;           // Текущее действие
    float XPos, YPos, ZPos; // Текущие координаты
    float Direction;        // Направление персонажа
    long  LastAnim;         // Последняя анимация
    long  LastAnimTime;     // Последнее время анимации

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

Для постоянного прекращения обновления персонажа вы используете вторую переменную с именем Locked. Если вы присвоите Locked значение TRUE, контроллер персонажа не будет обновлять персонаж, пока вы не установите Locked в FALSE.

Вы определяете ActionTimer и Locked в структуре sCharacter так:

    BOOL Locked;      // Блокировка определенных действий
    long ActionTimer; // Таймер обратного отсчета времени блокировки

Следующий набор переменных заботится о боевой стороне персонажей:

    sCharacter *Attacker;            // Атакующий персонаж (если есть)
    sCharacter *Victim;              // Атакуемый персонаж

    long SpellNum;                   // Заклинание для произнесения
                                     // при готовности
    long SpellTarget;                // Тип цели заклинания
    float TargetX, TargetY, TargetZ; // Координаты цели заклинания

Когда один персонаж атакует другого, указатели на атакующий персонаж и на жертву сохраняются в их соответствующих структурах sCharacter. Атакующий запоминает жертву, а жертва запоминает атакующего. Также, когда персонаж использует заклинание, сохраняется номер заклинания в MSL вместе с координатами цели заклинания и типом персонажа-цели (CHAR_PC, CHAR_NPC или CHAR_MONSTER).

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

То же самое относится к заклинаниям; как только персонаж произносит заклинание, информация о заклинании из структуры sCharacter используется для определения на кого или что было оказано воздействие. То же самое касается использования предметов; указатель на используемый предмет сохраняется в течение действия использования предмета вместе с указателем на структуру ICS персонажа cCharItem (для удаления предмета, если он помечен как USEONCE).

    long      ItemNum;   // Предмет, используемый при готовности
    sCharItem *CharItem; // Предмет для удаления из имущества

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

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

    float Distance;           // Расстояние преследования/избегания
    sCharacter *TargetChar;   // Преследуемый персонаж
    float MinX, MinY, MinZ;   // Минимальные ограничивающие координаты
    float MaxX, MaxY, MaxZ;   // Максимальные ограничивающие координаты

    long        NumPoints;    // Кол-во точек в маршруте
    long        CurrentPoint; // Текущая точка маршрута
    sRoutePoint *Route;       // Маршрутные точки

Далее располагается трио переменных, используемых для хранения короткого сообщения, которое накладывается поверх персонажа в процессе игры (как показано на рис. 12.15):

    char     Message[128]; // Текстовое сообщение
    long     MessageTimer; // Таймер текстового сообщения
    D3DCOLOR MessageColor; // Цвет текстового сообщения

Рис. 12.15. Механизм сообщений, как показано здесь, отображает результат определенных действий

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


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

Завершает класс sCharacter переменная объекта графического ядра cObject, которая поддерживает сетку и анимацию персонажа. Для улучшения визуального представления персонажа отдельный объект и сетка используются для представления оружия персонажа. Эта сетка оружия и объект конфигурируются каждый раз, когда персонаж экипируется новым оружием. Последними идут указатели связанного списка Prev и Next:

    cObject Object;          // Объект класса персонажа
    cMesh WeaponMesh;        // Сетка оружия
    cObject WeaponObject;    // Объект оружия

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

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

    sCharacter()
    {
        Definition = 0;  // Устанавливаем определение #0
        ID = -1;         // Нет ID
        Type = CHAR_NPC; // Персонаж NPC
        Enabled = FALSE; // Не активен

        Ailments = 0;    // Нет доп. статусов
        Charge = 0.0f;   // Нет заряда

        // Очищаем определение
        ZeroMemory(&Def, sizeof(sCharacterDefinition));
        CharICS = NULL; // Нет ICS

        ScriptFilename[0] = 0; // Нет скрипта

        Action = CHAR_IDLE; // Устанавливаем анимацию по умолчанию
        LastAnim = -1;      // Сбрасываем анимацию

        Locked = FALSE;     // Не заблокирован
        ActionTimer = 0;    // Нет таймера действия

        Attacker = NULL;    // Нет атакующего
        Victim = NULL;      // Нет жертвы

        ItemNum = 0;        // Нет предмета для использования
        CharItem = NULL;    // Нет имущества

        Distance = 0.0f;    // Задаем расстояние
        TargetChar = NULL;  // Нет целевого персонажа

        // Очищаем ограничивающий прямоугольник
        // (для ограничения перемещений)
        MinX = MinY = MinZ = MaxX = MaxY = MaxZ = 0.0f;

        NumPoints = 0;      // Нет маршрутных точек
        Route = NULL;       // Нет маршрута

        Message[0] = 0;     // Очищаем сообщение
        MessageTimer = 0;   // Сбрасывает таймер сообщения

        Prev = Next = NULL; // Очищаем указатели связанного списка
    }

    ~sCharacter()
    {
        if(CharICS != NULL) { // Освобождаем ICS персонажа
            CharICS->Free();
            delete CharICS;
        }
        delete [] Route;     // Освобождаем маршрут

        WeaponObject.Free(); // Освобождаем объект оружия
        WeaponMesh.Free();   // Освобождаем сетку оружия
        Object.Free();       // Освобождаем объект персонажа

        delete Next;         // Удаляем следующий персонаж в списке
    }
} sCharacter;

Вот и все! Я говорил вам, что структура sCharacter большая, но она ничто в сравнении с использующим эту структуру классом контроллера персонажей.

Класс sCharacterController

Мозг, оперирующий с персонажами, — это класс cCharacterController (содержащийся в файлах Chars.h и Chars.cpp), возможно, самый большой класс неигрового ядра, с которым вы будете работать. Из-за ограниченного пространства, вместо того чтобы показать определение класса здесь, я поместил код класса на прилагаемый к книге CD-ROM (смотрите в \BookCode\Chap12\Chars).

Класс cCharacterController поддерживает список активных персонажей, каждый из которых хранится в структуре sCharacter. Для каждого типа персонажей есть соответствующий элемент в массиве структур sCharacterMeshList (и соответствующая структура sCharAnimationInfo).

В начале файла Char.h расположено макроопределение:

// Количество персонажей в файле
#define NUM_CHARACTER_DEFINITIONS 256

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

// Типы персонажей
#define CHAR_PC      0
#define CHAR_NPC     1
#define CHAR_MONSTER 2

// Типы AI
#define CHAR_STAND  0
#define CHAR_WANDER 1
#define CHAR_ROUTE  2
#define CHAR_FOLLOW 3
#define CHAR_EVADE  4

// Дополнительные статусы
#define AILMENT_POISON          1
#define AILMENT_SLEEP           2
#define AILMENT_PARALYZE        4
#define AILMENT_WEAK            8
#define AILMENT_STRONG         16
#define AILMENT_ENCHANTED      32
#define AILMENT_BARRIER        64
#define AILMENT_DUMBFOUNDED   128
#define AILMENT_CLUMSY        256
#define AILMENT_SUREFOOTED    512
#define AILMENT_SLOW         1024
#define AILMENT_FAST         2048
#define AILMENT_BLIND        4096
#define AILMENT_HAWKEYE      8192
#define AILMENT_SILENCED    16384

// Типы действий/анимаций
#define CHAR_IDLE     0
#define CHAR_MOVE     1
#define CHAR_ATTACK   2
#define CHAR_SPELL    3
#define CHAR_ITEM     4
#define CHAR_HURT     5
#define CHAR_DIE      6
#define CHAR_TALK     7

Все остальное составляет класс контроллера.

class cCharacterController
{
  private:
    cGraphics *m_Graphics; // Родительский графический объект
    cFont     *m_Font;     // Используемый объект шрифта
    cFrustum  *m_Frustum;  // Пирамида видимого пространства

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

Далее идут имя файла MCL, указатели на MIL и MSL, и, наконец, указатель на контроллер заклинаний:

    char m_DefinitionFile[MAX_PATH]; // Имя файла определений

    sItem *m_MIL;   // Главный список предметов
    sSpell *m_MSL;  // Главный список заклинаний

    cSpellController *m_SpellController; // Контроллер заклинаний

По мере того, как персонажи добавляются к игре, счетчик (m_NumCharacters) отслеживает их общее количество. За счетчиком следует указатель на родительскую (корневую) структуру sCharacter в связанном списке структур:

    long m_NumCharacters;          // Кол-во персонажей в списке
    sCharacter *m_CharacterParent; // Список персонажей

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

    long m_NumMeshes;             // Кол-во используемых сеток
    sCharacterMeshList *m_Meshes; // Список сеток
    char m_MeshPath[MAX_PATH];    // Путь к сеткам оружия
    char m_TexturePath[MAX_PATH]; // Путь к текстурам сеток

    long               m_NumAnimations; // Кол-во анимаций
    sCharAnimationInfo *m_Animations;  // Данные анимаций

На этом внутренние данные класса sCharacterController завершаются. Теперь вы можете переключить внимание на закрытые функции. Вы используете первую функцию, GetXZRadius, для вычисления максимального ограничивающего радиуса вдоль осей X и Z.

    // Возвращает радиус X/Z персонажа
    float GetXZRadius(sCharacter *Character);

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


Рис. 12.16. Персонаж слева - высокий парень

Рис. 12.16. Персонаж слева — высокий парень. Из-за его высоты ограничивающая сфера объекта персонажа распространяется далеко в каждом направлении, вызывая ложные результаты проверки столкновений, когда два персонажа встречаются


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

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

    // Переопределяемая функция для воспроизведения звуков
    virtual BOOL ActionSound(sCharacter *Character)
        { return TRUE; }

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

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

    // Функция перемещения для персонажа игрока
    // (надо переопределить)
    virtual BOOL PCUpdate(
                 sCharacter *Character, long Elapsed, 
                 float *XMove, float *YMove, float *ZMove)
        { return TRUE; }

    // Функция обновления персонажа
    // для всех независимых персонажей
    BOOL CharUpdate(sCharacter *Character, long Elapsed,
                    float *XMove, float *YMove, float *ZMove);

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

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

    // Проверка допустимости перемещений.
    // Контроль границ для других персонажей и вызов
    // ValidateMove (переопределяемой).
    BOOL CheckMove(sCharacter *Character,
                   float *XMove, float *YMove, float *ZMove);

    // Виртуальная ValidateMove для сверки внешних границ
    // с перемещениями персонажа
    virtual BOOL ValidateMove(sCharacter *Character,
                   float *XMove, float *YMove, float *ZMove)
        { return TRUE; }

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

    // Завершение перемещения путем установки направления,
    // анимации и т.д.
    BOOL ProcessUpdate(sCharacter *Character,
                       float XMove, float YMove, float ZMove);

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

    // Обработка смерти персонажа игрока
    virtual BOOL PCDeath(sCharacter *Character)
        { return TRUE; }

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

    // Функции для роняния денег и предметов при гибели персонажа
    virtual BOOL DropMoney(float XPos,float YPos,float ZPos,
                           long Quantity)
        { return TRUE; }

    virtual BOOL DropItem(float XPos, float YPos, float ZPos,
                          long ItemNum)
        { return TRUE; }

Вы почти достигли конца долгого пути. Мы закончили с закрытыми данными и функциями, и нам остались открытые функции:

  public:
    cCharacterController();  // Конструктор
    ~cCharacterController(); // Деструктор

    // Функции для инициализации/отключения класса контроллера
    BOOL Init(cGraphics *Graphics, cFont *Font, 
              char *DefinitionFile, sItem *MIL, sSpell *MSL,
              long NumCharacterMeshes, char **MeshNames,
              char *MeshPath, char *TexturePath,
              long NumAnimations, sCharAnimationInfo *Anims,
              cSpellController *SpellController);
    BOOL Shutdown();

За типовыми конструктором и деструктором класса идет пара функций Init и Shutdown. Чтобы контроллер заработал вы должны сперва инициализировать его, вызвав Init. Когда вы завершаете работу с классом контроллера персонажей, вызовите в свою очередь Shutdown.

Здесь много аргументов, но каждый из них понятен. У вас есть родительский графический объект и объект шрифта, за которыми следует имя файла с определениями MCL. Далее идут указатели на главный список предметов и MSL. Помните, что MSL поддерживается контроллером заклинаний, так что для получения указателя на список необходимо вызвать sSpellController::GetSpell.

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

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

    // Освобождение класса
    BOOL Free();

Если говорить о добавлении персонажей к списку, вот функция, которая делает все необходимое для этого:

    // Добавление персонажа к списку
    BOOL Add(long IDNum, long Definition, long Type, long AI,
             float XPos, float YPos, float ZPos,
             float Direction = 0.0f);

    // Удаление персонажа из списка
    BOOL Remove(long IDNum);
    BOOL Remove(sCharacter *Character);

Для функции Add вам необходимо предоставить уникальный идентификационный номер, номер используемого определения персонажа в MCL, назначаемый персонажу тип (CHAR_PC, CHAR_NPC или CHAR_MONSTER), используемый искусственный интеллект, координаты персонажа и угол относительно оси Y, ориентирующий персонаж в заданном направлении.

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

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

    // Сохранение и загрузка отдельного персонажа
    BOOL Save(long IDNum, char *Filename);
    BOOL Load(long IDNum, char *Filename);

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

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

    // Обновление всех персонажей
    // на основе прошедшего времени
    BOOL Update(long Elapsed);

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

    // Визуализируем все объекты внутри
    // пирамиды видимого пространства
    BOOL Render(long Elapsed = -1,
                cFrustum *Frustum = NULL,
                float ZDistance = 0.0f);

У Render есть несколько необязательных аргументов. Первый из них вы используете чтобы управлять синхронизацией анимации персонажей. В средах с переключением задач, таких как Windows, простое использование времени, прошедшего с обработки последнего кадра недопустимо; вместо этого вы должны определить фиксированный период прошедшего времени и гарантировать, что игровой движок будет придерживаться обновлений с заданной частотой. В этой книге я обычно использую частоту обновлений в 30 кадров в секунду (задержка в 33 миллисекунды между кадрами).

Что касается указателя на пирамиду видимого пространства, приложение может предоставить собственный предварительно созданный объект, либо передать NULL (и необязательное расстояние по оси Z), чтобы класс создал свою собственную пирамиду видимого пространства.

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

    // Получение структуры sCharacter
    sCharacter *GetParentCharacter();
    sCharacter *GetCharacter(long IDNum);

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

    // Проверяет линию взгляда для атаки/заклинания
    virtual BOOL LineOfSight(
            sCharacter *Source, sCharacter *Target,
            float SourceX, float SourceY, float SourceZ,
            float TargetX, float TargetY, float TargetZ)
        { return TRUE; }

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

    // Функции для получения настроенных способностей
    float GetSpeed(sCharacter *Character);
    long  GetAttack(sCharacter *Character);
    long  GetDefense(sCharacter *Character);
    long  GetAgility(sCharacter *Character);
    long  GetResistance(sCharacter *Character);
    long  GetMental(sCharacter *Character);
    long  GetToHit(sCharacter *Character);
    float GetCharge(sCharacter *Character);

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

    // Установка указателя на ICS персонажа
    cCharICS *GetICS(long IDNum);

    // Установка блокировки и таймера действия
    BOOL SetLock(long IDNum, BOOL State);
    BOOL SetActionTimer(long IDNum, long Timer);

    // Установка расстояния избегания/следования
    BOOL SetDistance(long IDNum, float Distance);
    float GetDistance(long IDNum);

    // Установка маршрутных точек
    BOOL SetRoute(long IDNum,
                  long NumPoints, sRoutePoint *Route);

    // Установка скрипта
    BOOL SetScript(long IDNum, char *ScriptFilename);
    char *GetScript(long IDNum);

    // Установка флага разрешения
    BOOL SetEnable(long IDNum, BOOL Enable);
    BOOL GetEnable(long IDNum);

    // Функции для перемещения и получения координат персонажа
    BOOL Move(long IDNum, float XPos, float YPos, float ZPos);
    BOOL GetPosition(long IDNum,
                     float *XPos, float *YPos, float *ZPos);

    // Функции для установки/получения
    // границ перемещения персонажа
    BOOL SetBounds(long IDNum,
                   float MinX, float MinY, float MinZ,
                   float MaxX, float MaxY, float MaxZ);
    BOOL GetBounds(long IDNum,
                   float *MinX, float *MinY, float *MinZ,
                   float *MaxX, float *MaxY, float *MaxZ);

    // Функции для установки/получения типа персонажа
    BOOL SetType(long IDNum, long Type);
    long GetType(long IDNum);

    // Функции для установки получения AI персонажа
    BOOL SetAI(long IDNum, long Type);
    long GetAI(long IDNum);

    // Установка целевого персонажа
    BOOL SetTargetCharacter(long IDNum, long TargetNum);

Опуская детали предыдущих функций (обратитесь к разделу «Перемещение персонажей» за информацией об их функциональности), вы сталкиваетесь с функцией, используемой для установки данных, которые будут отображаться в сообщении поверх персонажа:

    // Установка текста сообщения, плавающего над персонажем
    BOOL SetMessage(sCharacter *Character, char *Text,
                    long Timer, D3DCOLOR Color=0xFFFFFFFF);

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

Далее следует функция, обрабатывающая ущерб, полученный от физической или магической атаки (это определяется флагом PhysicalAttack; значение TRUE соответствует физической атаке, а FALSE — магической):

    // Обработка ущерба, нанесенного заклинанием
    // или физической атакой
    BOOL Damage(sCharacter *Victim,
                BOOL PhysicalAttack, long Amount,
                long DmgClass, long CureClass);

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

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

    // Обработка смерти NPC/врага
    BOOL Death(sCharacter *Attacker, sCharacter *Victim);

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

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

    // Обработка увеличения опыта
    virtual BOOL Experience(sCharacter *Character,
                            long Amount)
        { Character->Def.Experience += Amount; return TRUE; }

Обратите внимание, что функция Experience может быть переопределена. Это может происходить, когда вы используете отдельный движок боевых последовательностей; вы не хотите добавлять опыт PC, пока битва не будет закончена. Соответственно вы используете свою собственную функцию чтобы отслеживать, сколько опыта добавить, когда сражение окончено.

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

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

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

    // Обработка физической атаки от атакующего к жертве
    BOOL Attack(sCharacter *Attacker, sCharacter *Victim);

    // Обработка последствий заклинания, когда оно завершено
    BOOL Spell(sCharacter *Caster,
               sSpellTracker *SpellTracker, sSpell *Spells);

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

    // Применение эффектов заклинания
    BOOL SpellEffect(sCharacter *Caster, sCharacter *Target,
                     sSpell *Spell);

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

    // Обработка экипировки/разэкипировки предмета
    BOOL Equip(sCharacter *Character, long ItemNum,
               long Type, BOOL Equip);

    // Обработка использования предмета персонажем
    BOOL Item(sCharacter *Owner, sCharacter *Target,
              long ItemNum, sCharItem *CharItem = NULL);

    // Обработка выбрасывания предмета
    BOOL Drop(sCharacter *Character,
              sCharItem *Item, long Quantity);

Для Equip вы должны указать модифицируемый персонаж и номер предмета (из MIL), который включается в экипировку. Вы используете аргумент Type, чтобы задать тип предмета экипировки (WEAPON, ARMOR, SHIELD или ACCESSORY) и флаг Equip, чтобы сообщить контроллеру, надо ли экипировать персонаж указанным предметом (Equip равно TRUE) или удалить из экипировки используемый в данный момент предмет (установив Equip в FALSE).

Для функции использования предмета (Item) требуются два персонажа: владелец предмета и персонаж, для которого этот предмет используется. Благодаря этому один персонаж может использовать лечебный эликсир для другого персонажа. Укажите номер используемого предмета в MIL, а также указатель на структуру CharItem ICS владельца, чтобы могло быть уменьшено количество предметов.

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

    // Обработка заклинания телепортации PC
    virtual BOOL PCTeleport(sCharacter *Character,
                            sSpell *Spell)
        { return TRUE; }

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

    // Установка действия (с таймером)
    BOOL SetAction(sCharacter *Character,
                   long Action, long AddTime = 0);
};

Когда PC (как, впрочем, и любой персонаж) делает что-нибудь, выполняется соответствующее действие. Ходьба — это действие, атака — это действие и т.д. Ранее эти действия были определены как CHAR_IDLE, CHAR_MOVE, CHAR_ATTACK и т.д. Вам необходимо задать в аргументе Action одно из этих значений для инициации действия персонажа.

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

Последний аргумент в списке, AddTime, применяется для добавления дополнительных миллисекунд к таймеру действия. Задание для AddTime значения –1 заставляет SetAction не использовать таймер действия, а это значит, что действие будет очищено при следующем обновлении.

Использование sCharacterController

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

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

char *g_CharMeshNames[] = {
    { "..\\Data\\Warrior.x" }, // Сетка # 0
    { "..\\Data\\Yodan.x" }    // Сетка # 1
};

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

sCharAnimationInfo g_CharAnimations[] = {
    { "Idle", TRUE },   // Действие CHAR_IDLE
    { "Walk", TRUE },   // Действие CHAR_MOVE
    { "Swing", FALSE }, // Действие CHAR_ATTACK
    { "Spell", FALSE }, // Действие CHAR_SPELL
    { "Swing", FALSE }, // Действие CHAR_ITEM
    { "Hurt", FALSE },  // Действие CHAR_HURT
    { "Die", FALSE },   // Действие CHAR_DIE
    { "Idle", TRUE }    // Действие CHAR_TALK
};

 

ВНИМАНИЕ!
Заметьте, что в структурах данных анимации я повторно использовал некоторые анимации, что исключительно здорово. Просто убедитесь, что вы не устанавливаете флаг зацикливания анимации в TRUE, если позже устанавливаете флаг зацикливания в FALSE, иначе все не будет работать. То же самое применимо, когда вы сперва устанавливаете для другого действия флаг зацикливания в FALSE, а позже устанавливаете его в TRUE.
Например, следующий фрагмент кода правильный:
sCharAnimationInfo Anims[] = {
    { "Idle", TRUE }, { "Walk", FALSE }
};
В то время как следующий фрагмент не работает (поскольку вы меняете флаг зацикливания у одной и той же используемой анимации):
sCharAnimationInfo Anims[] = {
    { "Idle", TRUE }, { "Idle", FALSE }
};

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

// Graphics = ранее инициализированный объект cGraphics
// Font = ранее инициализированный объект шрифта
// MIL = массив главного списка предметов (sItem MIL[1024])
// SpellController = ранее созданный контроллер заклинаний

cCharacterController Controller;

// Инициализация контроллера
Controller.Init(&Graphics, &Font, "..\\Data\\Default.mcl",
                (sItem*)&MIL, SpellController.GetSpell(0),
                sizeof(g_CharMeshNames)/sizeof(char*), g_CharMeshNames,
                "..\\Data\\", "..\\Data\\",
                sizeof(g_CharAnimations) / sizeof(sCharAnimationInfo),
                (sCharAnimationInfo*)&g_CharAnimations,
                &SpellController);

// Добавляем NPC (определение в MCL #0), который бродит
// внутри области от -256,0,-256 до 256,0,256
Controller.Add(0, 0, CHAR_NPC, CHAR_WANDER,
               0.0f, 0.0f, 0.0f, 0.0f);
Controller.SetBounds(0, -256.0f, 0.0f, -256.0f,
                     256.0f, 0.0f, 256.0f);

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

long UpdateCounter = timeGetTime(); // Записываем текущее время

// Для примера устанавливаем действие атаки
Controller.GetCharacter(0)->Victim = FALSE;
Controller.SetAction(Controller.GetCharacter(0), CHAR_ATTACK);

// Присоединяем оружие к NPC (предмет #0 - меч)
Controller.Equip(Controller.GetCharacter(0), 0, WEAPON, TRUE);

while(1) {
    // Ограничиваем обновления каждыми 33 миллисекундами
    while(timeGetTime() < UpdateCounter + 33);

    UpdateCounter = timeGetTime(); // Записываем новое время

    Controller.Update(33);         // Принудительное обновление
                                   // на 33 миллисекунды
    Graphics.Clear();
    if(Graphics.BeginScene() == TRUE) {
        // Обновление анимации персонажа на 33 миллисекунды
        // и визуализация ее на экране
        Controller.Render(33);
    }
    Graphics.Display();
}

Этот краткий пример демонстрирует основы использования контроллера. Более сложный пример можете посмотреть в демонстрационной программе Chars.


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

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