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

Программирование примера игры

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


Рис. 16.7. Проект вашей игры разделен на части, подобно паззлу

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


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

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

В таблице 16.4 описаны компоненты, используемые в The Tower, и указаны главы, в которых эти компоненты были разработаны.


Таблица 16.4. Компоненты игры The Tower



Компонент Описание

Ядро игры Используется каждый компонент ядра игры, за исключением сетевого ядра. Конкретнее, это следующие компоненты: графическое ядро, системное ядро и ядро ввода. Целиком ядро игры рассматривалось в главе 6, «Создаем ядро игры»
Пирамида видимого пространства и текстовые окна Компонент пирамиды видимого пространства из главы 8, «Создание трехмерного графического движка», используется для отбрасывания невидимых объектов до того, как они будут визуализированы. Кроме этого, класс текстового окна из главы 12 применяется для отображения диалогов и других текстов в игре.
Движок смешанной 2D/3D графики Это тот же самый графический движок, который был разработан в главе 9. Он позволяет визуализировать трехмерные сетки поверх заранее визуализированных двухмерных фоновых изображений.
Скрипты и контроллер скриптов Для разработки игровых скриптов используется система Mad Lib Script, созданная в главе 10. Класс контроллера скриптов из главы 12 применяется для загрузки и обработки этих скриптов в игре.
Предметы и список имущества Главный список предметов в комбинации с системой управления имуществом персонажа, оба из главы 11, «Определение и использование объектов».
Персонажи и контроллер персонажей Для управления персонажами и их визуализации в игре используется полный контроллер персонажей (с поддержкой главного списка персонажей), показанный в главе 12.
Заклинания и контроллер заклинаний Контроллер заклинаний из главы 12 применяется для управления заклинаниями и их отображения. Главный список заклинаний (также описанный в главе 12) используется для определения заклинаний в игре.
Барьеры и триггеры Барьеры блокируют перемещение, а триггеры исполняют скрипты, когда их кто-то коснется; обе системы обсуждались в главе 13, «Работа с картами и уровнями».


Проект с примером игры (\BookCode\Chap16\The Tower\) состоит из перечисленных ниже файлов, представляющих игровые компоненты из таблицы 16.4.

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

Структурирование приложения

Главное приложение относительно небольшое (если вы можете назвать небольшим примерно 1500 строк кода). Его работа — инициализация всех необходимых компонентов и отслеживание состояния игры (верно, здесь используется обработка на основе состояний).

Сперва вы объявляете класс приложения. Хотя на данный момент класс незавершен, в оставшейся части главы все части займут свои места и класс приложения станет законченным. Сейчас посмотрим на разделы класса приложения где устанавливаются данные класса и инициализируется игровая система:

class cApp : public cApplication
{
    friend class cSpells;
    friend class cChars;
    friend class cGameScript;

 

СОВЕТ
Когда один класс объявляет другой как друга класса, как сделано здесь, этот дружественный класс получает неограниченный доступ к данным объявляющего класса.

Класс приложения начинается с установки ссылок для трех друзей класса. Эти три класса, cSpells, cChars и cGameScript, являются производными классами контроллеров для заклинаний, персонажей и скриптов, соответственно. Каждому из этих классов необходим особый доступ к классу приложения, поэтому мы делаем их друзьями. В следующей части класса cApp объявляется список относящихся к ядру игры объектов, которые все являются закрытыми членами класса cApp:

  private:
    // Графическое устройство, камера и шрифт
    cGraphics m_Graphics;
    cCamera   m_Camera;
    cFont     m_Font;

    // Система ввода и устройства
    cInput       m_Input;
    cInputDevice m_Keyboard;
    cInputDevice m_Mouse;

    // Звуковая система, звуковые и музыкальные каналы
    // и звуковые данные 
    cSound        m_Sound;
    cSoundChannel m_SoundChannel;
    cMusicChannel m_MusicChannel;
    cSoundData    m_SoundData;

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

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

    // Текстура с растровым изображением
    cTexture m_Options;

    // Текстовые окна
    cWindow m_Stats;
    cWindow m_Window;
    cWindow m_Header;

В этой точке объявления класса вы определяете пару вспомогательных закрытых функций:

    BOOL WinGame(); // Обработка сценария завершения игры

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

Функция WinGame вызывается всякий раз, когда скрипт сталкивается с действием завершения игры. Это действие запускает окончание игры, которое возвращает управление к главному меню. GetCharacterAt — это функция (обсуждавшаяся в главе 14, «Создание боевых последовательностей»), определяющая по какому из персонажей игрок щелкнул мышью.

Завершают cApp конструктор класса и переопределенные функции Init, Shutdown и Frame, все объявленные открытыми:

  public:
    cApp();

    // Переопределенные функции
    BOOL Init();
    BOOL Shutdown();
    BOOL Frame();
};

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

Конструктор cApp

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

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

Функция приложения Init

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

BOOL cApp::Init()
{
    // Инициализация графического устройства
    m_Graphics.Init();

    // Определяем, используется ли полноэкранный режим
#ifdef FULLSCREENMODE
    m_Graphics.SetMode(GethWnd(), FALSE, TRUE, 640, 480, 16);
#else
    m_Graphics.SetMode(GethWnd(), TRUE, TRUE);
#endif

    // Установка перспективы
    m_Graphics.SetPerspective(0.6021124f,1.33333f,1.0f,20000.0f);

    // Разрешаем отображение курсора
    ShowMouse(TRUE);

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

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

Далее вы инициализируете систему ввода и создаете два интерфейса устройств — один для клавиатуры и другой для мыши:

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

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

    // Инициализация звуковой системы и каналов
    m_Sound.Init(GethWnd(), 22050, 1, 16);
    m_SoundChannel.Create(&m_Sound, 22050, 1, 16);
    m_MusicChannel.Create(&m_Sound);
Теперь вы инициализируете относящиеся к игре данные и интерфейсы. Вы загружаете главный список предметов (находящийся в каталоге \BookCode\Chap16\Data) и инициализируете контроллер персонажей и контроллер заклинаний:

    // Загрузка главного списка предметов
    FILE *fp;

    // Обнуляем память, используемую для хранения данных предметов
    for(long i = 0; i < 1024; i++)
        ZeroMemory(&m_MIL[i], sizeof(sItem));

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

    // Инициализация контроллера персонажей
    m_CharController.SetData(this);
    m_CharController.Init(&m_Graphics, &m_Font,
               "..\\Data\\Game.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.SetData(this);
    m_SpellController.Init(&m_Graphics,
               "..\\Data\\Game.msl",
               sizeof(g_SpellMeshNames)/sizeof(char*),g_SpellMeshNames,
               "..\\Data\\", &m_CharController);

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

    // Получаем растровое изображение параметров
    m_Options.Load(&m_Graphics, "..\\Data\\Options.bmp");

    // Создаем главное окно, заголовок и окно статистики 
    m_Window.Create(&m_Graphics, &m_Font);
    m_Header.Create(&m_Graphics, &m_Font);
    m_Stats.Create(&m_Graphics, &m_Font);

    // Позиционируем все окна
    m_Window.Move(2,2, 636, 476);
    m_Header.Move(2,2,128,32,-1,-1,D3DCOLOR_RGBA(128,16,16,255));
    m_Stats.Move(2,2,128,48);

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

    // Устанавливаем указатель на приложение для скрипта
    m_Script.SetData(this);

    // Помещаем в стек состояние главного меню,
    // предварительно установив параметры меню 
    g_MenuOptions = MENU_LOAD;
    m_StateManager.Push(MenuFrame, this);

    return TRUE;
}

Функция Shutdown

Что хорошего в функции Init без соответствующей функции Shutdown для отключения и освобождения используемых в игре ресурсов? Функция cApp::Shutdown делает следующее: очищает контроллеры, удаляет состояния из стека состояний, освобождает скрипты и так далее:

BOOL cApp::Shutdown()
{
    // Извлекаем все состояния
    m_StateManager.PopAll(this);

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

    // Освобождаем объект скриптов
    m_Script.Free();

    // Освобождаем данные уровня
    FreeLevel();

    // Освобождаем текстуру параметров
    m_Options.Free();

    // Освобождаем текстовые окна
    m_Window.Free();
    m_Header.Free();
    m_Stats.Free();

    // Отключаем звук
    m_MusicChannel.Free();
    m_SoundChannel.Free();
    m_Sound.Shutdown();

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

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

    return TRUE;
}

Обработка кадров в функции Frame

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

BOOL cApp::Frame()
{
    static DWORD UpdateTimer = timeGetTime();

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

Как я упоминал, игра обновляется 30 раз в секунду. В каждом кадре обновления игры считывается состояние клавиатуры и мыши и обрабатывается текущее игровое состояние:

    // Захватываем устройства ввода и считываем с них
    // данные для всех состояний
    m_Keyboard.Acquire(TRUE); // Чтение клавиатуры
    m_Keyboard.Read();
    m_Mouse.Acquire(TRUE); // Чтение мыши
    m_Mouse.Read();

    // Обработка состояния, возвращение результата
    return m_StateManager.Process(this);
}

Глава 1, «Подготовка к работе с книгой», рассказывает как работает обработка на основе состояний. Поскольку состояния помещаются в стек состояний, при вызове cStateManager::Process, который вы можете видеть в функции Frame, исполняется самое верхнее состояние.

Использование обработки на основе состояний

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

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

// Диспетчер обработки состояний и функции состояний
cStateManager m_StateManager;

static void MenuFrame(void *Ptr, long Purpose);
static void GameFrame(void *Ptr, long Purpose);
static void StatusFrame(void *Ptr, long Purpose);
static void BarterFrame(void *Ptr, long Purpose);

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

void cApp::GameFrame(void *Ptr, long Purpose)
{
    cApp *App = (cApp*)Ptr;
    sCharacter *CharPtr;
    BOOL MonstersInLevel;
    long TriggerNum;
    char Filename[MAX_PATH], Stats[256];
    float MaxY;

    // Обрабатываем только кадры
    if(Purpose != FRAMEPURPOSE)
        return;

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

ПРИМЕЧАНИЕ
В главе 1 вы читали об использовании целей вызова (calling purpose). При использовании функций состояния цель вызова информирует функцию, зачем она была вызвана: для инициализации данных, либо для обработки кадра, либо для выключения и освобождения ресурсов.

В начале функции GameFrame производится быстрая проверка, нажата ли клавиша Esc. Если да, то в стек помещается состояние главного меню:

    // Выход в экран меню, если нажата ESC
    if(App->m_Keyboard.GetKeyState(KEY_ESC) == TRUE) {
        // Установка параметров меню
        g_MenuOptions = MENU_BACK | MENU_SAVE | MENU_LOAD;

Чтобы знать, какие команды отображать в главном меню, вы объявляете в начале приложения глобальную переменную. Эта глобальная переменная, g_MenuOptions является битовым полем и использует следующие макроопределения для задания значений — MENU_BACK для отображения команды возврата к игре, MENU_SAVE для отображения команды сохранения игры и MENU_LOAD для отображения команды загрузки игры. Как только команды определены, состояние можно помещать в стек:

        // Помещаем в стек состояние главного меню
        App->m_StateManager.Push(App->MenuFrame, App);
        return;
    }

    // Если телепортировались, выполняем начальную обработку
    // и возвращаемся
    if(App->m_TeleportMap != -1) {
        // Освобождаем уровень и начинаем работу с новым
        App->FreeLevel();
        App->LoadLevel(App->m_TeleportMap);
        App->m_TeleportMap = -1; // Очищаем номер карты телепортирования
        return;                     // Больше нечего обрабатывать в этом кадре
    }

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

Мы добрались до основной части функции GameFrame. В начале следующего блока кода вы сбрасываете флаг, отслеживающий, есть ли на карте какие-либо монстры. Затем сканируется весь список загруженных персонажей. Если в списке персонажей вы находите монстра, флаг MonstersInLevel устанавливается. Также для каждого монстра на карте меняются параметры его искусственного интеллекта в зависимости от заряда. Для заряда меньше 70, поведение монстра устанавливается на хождение по карте (что позволяет монстру перемещаться по уровню). Если заряд больше 70, поведение монстра настраивается на преследование игрока (это означает, что монстр пытается атаковать игрока):

    // Отмечаем отсутствие монстров на уровне
    MonstersInLevel = FALSE;

    // Смотрим, есть ли какие-либо персонажи на уровне.
    // Если есть какие-либо монстры, отмечаем это и устанавливаем
    // их поведение на ходьбу по уровню, если заряд меньше 70
    // и на преследование в ином случае.
    // Также обрабатываем достижение персонажем маршрутной точки. 
    CharPtr = App->m_CharController.GetParentCharacter();
    while(CharPtr != NULL) {
        // Меняем поведение монстра в зависимости от заряда
        if(CharPtr->Type == CHAR_MONSTER) {
            MonstersInLevel = TRUE;

            // Меняем AI в зависимости от заряда
            if(CharPtr->Charge >= 70.0f) {
                CharPtr->AI = CHAR_FOLLOW;
                CharPtr->TargetChar = g_PCChar;
                CharPtr->Distance = 0.0f;
            } else {
                CharPtr->AI = CHAR_WANDER;
            }
        }

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

        // Проверяем, достиг ли NPC последней точки маршрута
        if(CharPtr->Type==CHAR_NPC && CharPtr->AI==CHAR_ROUTE) {
            // Была достигнута конечная точка?
            if(App->LastPointReached(CharPtr) == TRUE) {
                // Обрабатываем скрипт завершения маршрута персонажа
                sprintf(Filename,"..\\Data\\EOR%lu.mls", CharPtr->ID);
                App->m_Script.Execute(Filename);

                // Больше нечего обрабатывать в этом кадре
                return;
            }
        }

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

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

    // Обработка начала сражения
    if(MonstersInLevel == TRUE && App->m_MonstersLastFrame == FALSE)
        App->StartOfCombat();

    // Обработка конца сражения, если оно завершилось
    if(MonstersInLevel == FALSE && App->m_MonstersLastFrame == TRUE)
        App->EndOfCombat();

    // Запоминаем, были ли монстры в этом кадре
    // и восстанавливаем полный заряд игрока, если монстров нет
    if((App->m_MonstersLastFrame = MonstersInLevel) == FALSE)
        g_PCChar->Charge = 100.0f;

    // Обновление контроллеров
    App->m_CharController.Update(33);
    App->m_SpellController.Update(33);

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

После обновления персонажей в игру вступают объекты триггеров. Если игрок входит в активный триггер, исполняется соответствующий скрипт:

    // Проверка триггеров и исполнение скрипта
    if((TriggerNum = App->m_Trigger.GetTrigger(g_PCChar->XPos,
                                               g_PCChar->YPos,
                                               g_PCChar->ZPos))) {
        sprintf(Filename, "..\\Data\\Trig%lu.mls", TriggerNum);
        App->m_Script.Execute(Filename);

        return; // Больше нечего обрабатывать в этом кадре
    }

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

    // Местоположение камеры в сцене
    App->m_Graphics.SetCamera(&App->m_Camera);

    // Визуализируем все
    App->m_Graphics.ClearZBuffer();
    if(App->m_Graphics.BeginScene() == TRUE) {
        App->RenderFrame(33);

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

        // Визуализируем полосу заряда игрока,
        // но только во время битвы
        if(MonstersInLevel == TRUE) {
            D3DXMATRIX matWorld, matView, matProj;
            D3DVIEWPORT9 vpScreen;
            D3DXVECTOR3 vecPos;

            // Получаем мировое преобразование, преобразование вида
            // и преобразование проекции
            D3DXMatrixIdentity(&matWorld);
            App->m_Graphics.GetDeviceCOM()->GetTransform(
                                             D3DTS_VIEW, &matView);
            App->m_Graphics.GetDeviceCOM()->GetTransform(
                                             D3DTS_PROJECTION, &matProj);

            // Получаем порт просмотра
            App->m_Graphics.GetDeviceCOM()->GetViewport(&vpScreen);

            // Смещаем полосу заряда на высоту персонажа
            g_PCChar->Object.GetBounds(NULL, NULL, NULL,
                                       NULL, &MaxY, NULL, NULL);

            // Проецируем координаты на экран
            D3DXVec3Project(&vecPos, &D3DXVECTOR3(
                                     g_PCChar->XPos,
                                     g_PCChar->YPos + MaxY,
                                     g_PCChar->ZPos),
                            &vpScreen, &matProj, &matView, &matWorld);

            // Перемещаем на 8 пикселов вправо перед отображением
            vecPos.x += 8.0f;

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

Теперь вы отключаете Z-буфер и рисуете измеритель заряда, используя в качестве источника его изображения объект текстуры m_Option:

            // Отображаем полосу заряда рядом с игроком
            // (мерцает, если полная)
            App->m_Graphics.EnableZBuffer(FALSE);
            App->m_Graphics.BeginSprite();
            App->m_Options.Blit((long)vecPos.x, (long)vecPos.y,
                                0, 0, 16, 4);
            if(g_PCChar->Charge >= 100.0f) {
                if(timeGetTime() & 1)
                    App->m_Options.Blit((long)vecPos.x, (long)vecPos.y,
                                        0, 4, 16, 4);
            } else {
                App->m_Options.Blit((long)vecPos.x, (long)vecPos.y,
                         0, 4, (long)(g_PCChar->Charge/100.0f*16.0f), 4);
            }
            App->m_Graphics.EndSprite();
        }

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

        // Рисуем статистику игрока вверху слева
        sprintf(Stats, "%ld / %ld HP\r\n%ld / %ld MP",
                g_PCChar->HealthPoints, g_PCChar->Def.HealthPoints,
                g_PCChar->ManaPoints, g_PCChar->Def.ManaPoints);
        App->m_Stats.Render(Stats);
        App->m_Graphics.EndScene();
    }

    App->m_Graphics.Display();
}

Функция GameFrame завершается вызовом EndScene и отображением кадра пользователю. Остальные функции состояний по своей природе просты, поэтому я только кратко опишу их здесь, оставив их исследование вам, когда вы будете работать с примером игры на CD-ROM.

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

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

Работа с картами

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

Для загрузки и использования шести текстур объявите массив объектов cTexture (для хранения шести растровых изображений), объект cMesh (который содержит упрощенную сетку сцены) и объект cObject (используемый для визуализации упрощенной сетки):

long     m_SceneNum;         // Номер текущей сцены, 1-5
cTexture m_SceneTextures[6]; // Шесть текстур сцены
cMesh    m_SceneMesh;        // Упрощенная сетка сцены
cObject  m_SceneObject;      // Объект упрощенной сцены

Для работы со сценами используются четыре функции класса приложения. Это функции LoadLevel, FreeLevel, GetHeightBelow и CheckIntersect. Вы используете функции GetHeightBelow и CheckIntersect, представленные в главе 8, для проверки пересечения сетки с сеткой. В данном случае проверка пересечения сеток применяется для обнаружения тех случаев, когда персонаж пересекается с упрощенной сеткой сцены.

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

BOOL cApp::LoadLevel(long Num)
{
    char Filename[MAX_PATH];
    FILE *fp;
    long i;
    float XPos, YPos, ZPos, XAt, YAt, ZAt;

    FreeLevel(); // Освобождаем предыдущий уровень

    // Сохраняем номер сцены
    m_SceneNum = Num;

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

    // Загрузка фоновых текстур
    for(i = 0; i < 6; i++) {
        sprintf(Filename, "..\\Data\\Scene%u%u.bmp", Num, i+1);
        if(m_SceneTextures[i].Load(&m_Graphics, Filename) == FALSE)
            return FALSE;
    }

    // Загрузка сетки сцены и конфигурирование объекта
    sprintf(Filename, "..\\Data\\Scene%u.x", Num);
    if(m_SceneMesh.Load(&m_Graphics, Filename) == FALSE)
        return FALSE;
    m_SceneObject.Create(&m_Graphics, &m_SceneMesh);

Загрузив сетку сцены и создав объект сцены вы готовы определить местоположение камеры, используемой для визуализации трехмерной графики. Для размещения камеры в каждой из сцен вы заранее создаете текстовые файлы для каждой сцены. Имена этих файлов cam1.txt, cam2.txt, cam3.txt, cam4.txt и cam5.txt — каждое имя сформировано согласно номеру соответствующей сцены (сцены нумеруются от 1 до 5).

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

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

    // Загрузка данных камеры
    sprintf(Filename, "..\\Data\\Cam%u.txt", Num);
    if((fp=fopen(Filename, "rb")) == NULL)
        return FALSE;

    XPos = GetNextFloat(fp);
    YPos = GetNextFloat(fp);
    ZPos = GetNextFloat(fp);
    XAt  = GetNextFloat(fp);
    YAt  = GetNextFloat(fp);
    ZAt  = GetNextFloat(fp);

    fclose(fp);

    // Позиционирование камеры для сцены
    m_Camera.Point(XPos, YPos, ZPos, XAt, YAt, ZAt);
    m_Graphics.SetCamera(&m_Camera);

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

    // Нет монстров в последнем кадре
    m_MonstersLastFrame = FALSE;

    // Выполняем скрипт загрузки сцены
    sprintf(Filename, "..\\Data\\Scene%lu.mls", Num);
    m_Script.Execute(Filename);

    return TRUE;
}

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

BOOL cApp::FreeLevel()
{
    sCharacter *CharPtr, *NextChar;
    long i;

    // Освобождение сетки сцены и текстур 
    m_SceneMesh.Free();
    m_SceneObject.Free();
    for(i = 0; i < 6; i++)
        m_SceneTextures[i].Free();

    // Освобождение триггеров и барьеров
    m_Barrier.Free();
    m_Trigger.Free();

    // Освобождение всех независимых персонажей
    if((CharPtr = m_CharController.GetParentCharacter()) != NULL) {
        while(CharPtr != NULL) {
            // Запоминаем следующий персонаж
            NextChar = CharPtr->Next;

            // Удаляем независимый персонаж
            if(CharPtr->Type != CHAR_PC)
                m_CharController.Remove(CharPtr);

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

    // Освобождаем все эффекты заклинаний
    m_SpellController.Free();

    return TRUE;
}

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

В игре The Tower используются и барьеры и триггеры. Эти компоненты остались практически неизменными по сравнению с показанными в главе 13, так что можете обратиться к этой главе за более подробными сведениями об их использовании. Только скрипты имеют возможность добавлять барьеры и триггеры в игре. Вы объявляете объекты барьеров и триггеров в объявлении класса cApp следующим образом:

cTrigger m_Trigger;
cBarrier m_Barrier;

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

Управление персонажами

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

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

ПРИМЕЧАНИЕ
Производный класс контроллера персонажей находится в паре файлов с именами Game_Chars.h и Game_Chars.cpp.

class cChars : public cCharacterController
{
  private:
    cApp *m_App;

    BOOL PCUpdate(sCharacter *Character, long Elapsed,
                  float *XMove, float *YMove, float *ZMove);
    BOOL ValidateMove(sCharacter *Character,
                  float *XMove, float *YMove, float *ZMove);

    BOOL Experience(sCharacter *Character, long Amount);

    BOOL PCTeleport(sCharacter *Character, sSpell *Spell);

    BOOL ActionSound(sCharacter *Character);

    BOOL DropMoney(float XPos, float YPos, float ZPos,
                   long Quantity);
    BOOL DropItem(float XPos, float YPos, float ZPos,
                   long Item, long Quantity);

  public:
    BOOL SetData(cApp *App) { m_App = App; return TRUE; }
};

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

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

Прочие функции, такие как Experience, DropMoney и DropItem сообщают игровому движку что монстр был убит и игра должна вознаградить игрока очками опыта, а также деньгами и предметами, выроненными умирающим монстром. Эти награды копятся до конца сражения, и тогда их обрабатывает функция класса приложения EndOfCombat.

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

BOOL cChars::PCUpdate(sCharacter *Character, long Elapsed,
                      float *XMove, float *YMove, float *ZMove)
{
    float Speed;
    sCharacter *TargetChar;
    float XDiff, YDiff, ZDiff;
    float Dist, Range;
    char Filename[MAX_PATH];
    long Spell = -1;

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

Начинается PCUpdate с определения того, следует ли проводить обновление (основываясь на прошедшем времени) и продолжается определением того, какие клавиши были нажаты на клавиатуре (если это имело место). Если была нажата клавиша перемещения курсора вверх, персонаж перемещается вперед, а если были нажаты клавиши перемещения курсора влево или вправо, персонаж поворачивается, как показано ниже:

    // Не обновляем, если не прошло времени
    if(!Elapsed)
        return TRUE;

    // Поворот персонажа
    if(m_App->m_Keyboard.GetKeyState(KEY_LEFT) == TRUE) {
        Character->Direction -= (float)Elapsed / 1000.0f * 4.0f;
        Character->Action = CHAR_MOVE;
    }

    if(m_App->m_Keyboard.GetKeyState(KEY_RIGHT) == TRUE) {
        Character->Direction += (float)Elapsed / 1000.0f * 4.0f;
        Character->Action = CHAR_MOVE;
    }

    if(m_App->m_Keyboard.GetKeyState(KEY_UP) == TRUE) {
        Speed = (float)Elapsed / 1000.0f *
                    m_App->m_CharController.GetSpeed(Character);
        *XMove = (float)sin(Character->Direction) * Speed;
        *ZMove = (float)cos(Character->Direction) * Speed;
        Character->Action = CHAR_MOVE;
    }

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

Затем вы определяете, щелкнул ли игрок левой кнопкой мыши. Вспомните, что согласно проекту примера игры, щелчок левой кнопкой мыши по расположенному вблизи персонажу приводит либо к атаке персонажа (если этот персонаж монстр) или к разговору с персонажем (если персонаж NPC):

    // Обработка действия атаки/разговора
    if(m_App->m_Mouse.GetButtonState(MOUSE_LBUTTON) == TRUE) {
        // Смотрим, какой персонаж выбран и проверяем, является ли он монстром
        if((TargetChar = m_App->GetCharacterAt(
                             m_App->m_Mouse.GetXPos(),
                             m_App->m_Mouse.GetYPos())) != NULL) {

Показанный выше фрагмент кода просто вызывает функцию GetCharacterAt, сканирующую персонажей, расположенных под указателем мыши. Если персонаж найден, вы определяете его тип; для NPC вы выполняете соответствующий скрипт персонажа:

            // Обработка разговора с NPC
            if(TargetChar->Type == CHAR_NPC) {
                // Не проверяем расстояние, просто обрабатываем скрипт
                sprintf(Filename, "..\\Data\\Char%lu.mls",
                        TargetChar->ID);
                m_App->m_Script.Execute(Filename);

                return TRUE; // Больше нечего обрабатывать
            }

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

            // Обработка атаки монстра
            if(TargetChar->Type == CHAR_MONSTER) {
                // Получаем расстояние до цели
                XDiff = (float)fabs(TargetChar->XPos - Character->XPos);
                YDiff = (float)fabs(TargetChar->YPos - Character->YPos);
                ZDiff = (float)fabs(TargetChar->ZPos - Character->ZPos);
                Dist = XDiff*XDiff + YDiff*YDiff + ZDiff*ZDiff;

                // Смещаем расстояние на радиус цели
                Range = GetXZRadius(TargetChar);
                Dist -= (Range * Range);

                // Получаем максимальную дальность атаки
                Range = GetXZRadius(Character);
                Range += Character->Def.Range;

                // Выполняем атаку только если цель в заданном диапазоне
                if(Dist <= (Range * Range)) {
                    // Устанавливаем информацию цели/жертвы
                    TargetChar->Attacker = Character;
                    Character->Victim = TargetChar;

                    // Поворачиваемся к жертве
                    XDiff = TargetChar->XPos - Character->XPos;
                    ZDiff = TargetChar->ZPos - Character->ZPos;
                    Character->Direction = (float)atan2(XDiff, ZDiff);

                    // Устанавливаем действие
                    m_App->m_CharController.SetAction(Character,
                                                      CHAR_ATTACK);
                }
            }
        }
    }

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

    // Произносим заклинание, основываясь на нажатой цифре
    if(m_App->m_Keyboard.GetKeyState(KEY_1) == TRUE) {
        m_App->m_Keyboard.SetLock(KEY_1, TRUE);
        Spell = 0; // Огненный шар
    }

    if(m_App->m_Keyboard.GetKeyState(KEY_2) == TRUE) {
        m_App->m_Keyboard.SetLock(KEY_2, TRUE);
        Spell = 1; // Лед
    }

    if(m_App->m_Keyboard.GetKeyState(KEY_3) == TRUE) {
        m_App->m_Keyboard.SetLock(KEY_3, TRUE);
        Spell = 2; // Исцеление
    }

    if(m_App->m_Keyboard.GetKeyState(KEY_4) == TRUE) {
        m_App->m_Keyboard.SetLock(KEY_4, TRUE);
        Spell = 3; // Телепортация
    }

    if(m_App->m_Keyboard.GetKeyState(KEY_5) == TRUE) {
        m_App->m_Keyboard.SetLock(KEY_5, TRUE);
        Spell = 4; // Земляной вал
    }

    // Произносим заклинание, если скомандовали
    if(Spell != -1) {

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

        // Произносим только известные заклинания для которых достаточно маны
        if(g_PCChar->Def.MagicSpells[Spell/32] & (1 << (Spell & 31)) &&
                             g_PCChar->ManaPoints >=
                             m_App->m_SpellController.GetSpell(Spell)->Cost) {
            // Смотрим, на какого персонажа указали
            if((TargetChar = m_App->GetCharacterAt(
                             m_App->m_Mouse.GetXPos(),
                             m_App->m_Mouse.GetYPos())) != NULL) {
                // Не нацеливаемся на NPC
                if(TargetChar->Type != CHAR_NPC) {
                    // Получаем расстояние до цели
                    XDiff = (float)fabs(TargetChar->XPos - Character->XPos);
                    YDiff = (float)fabs(TargetChar->YPos - Character->YPos);
                    ZDiff = (float)fabs(TargetChar->ZPos - Character->ZPos);
                    Dist = XDiff*XDiff + YDiff*YDiff + ZDiff*ZDiff;

                    // Смещаем расстояние на радиус цели
                    Range = GetXZRadius(TargetChar);
                    Dist -= (Range * Range);

                    // Получаем максимальную дистанцию заклинания
                    Range = GetXZRadius(Character);
                    Range += m_App->m_SpellController.GetSpell(Spell)->Distance;

                    // Выполняем заклинание только если цель в заданном диапазоне
                    if(Dist <= (Range * Range)) {

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

                        // Установка данных заклинания
                        Character->SpellNum = Spell;
                        Character->SpellTarget = CHAR_MONSTER;

                        // Сохраняем координаты цели
                        Character->TargetX = TargetChar->XPos;
                        Character->TargetY = TargetChar->YPos;
                        Character->TargetZ = TargetChar->ZPos;

                        // Очищаем перемещение
                        (*XMove) = (*YMove) = (*ZMove) = 0.0f;

                        // Выполняем действия заклинания
                        SetAction(Character, CHAR_SPELL);

                        // Поворачиваемся к жертве
                        XDiff = TargetChar->XPos - Character->XPos;
                        ZDiff = TargetChar->ZPos - Character->ZPos;
                        Character->Direction = (float)atan2(XDiff, ZDiff);

                        // Устанавливаем действие
                        m_App->m_CharController.SetAction(Character,
                                                          CHAR_SPELL);
                    }
                }
            }
        }
    }

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

    // Помещаем кадр статистики, если нажата правая кнопка мыши
    if(m_App->m_Mouse.GetButtonState(MOUSE_RBUTTON) == TRUE) {
        m_App->m_Mouse.SetLock(MOUSE_RBUTTON, TRUE);
        m_App->m_StateManager.Push(m_App->StatusFrame, m_App);
    }

    return TRUE;
}

Для использования производного класса контроллера персонажей, игра создает экземпляр класса cChars в объявлении cApp:

cChars m_CharController;

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

ПРИМЕЧАНИЕ
Производный класс контроллера заклинаний находится в файлах Game_Spells.h и Game_Spells.cpp.

class cSpells : public cSpellController
{
  private:
    cApp *m_App;

  public:
    BOOL SetData(cApp *App) { m_App = App; return TRUE; }
    BOOL SpellSound(long Num);
};

Подобно производному контроллеру персонажей, производный контроллер заклинаний имеет функцию SetData, сообщающую контроллеру, к какому классу приложения обращаться. В функции SpellSound вы используете указатель на приложение для вызова функции cApp::PlaySound.

Надеюсь вы не моргали, поскольку это все, что относится к производному классу контроллера заклинаний! Единственная переопределяемая функция — та, которая воспроизводит звук; остальная функциональность поддерживается базовым классом cSpellController. Чтобы использовать производный класс в игре, вы объявляете его экземпляр в объявлении класса приложения:

cSpells m_SpellController;

Поддержка торговли

Ранее вы читали о том, как состояние BarterFrame используется для визуализации сцены торговли (рис. 16.8), где игрок может приобретать предметы у персонажа.


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

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


Как это состояние узнает, какие предметы продаются? В игре имеется единственный способ включить состояние торговли — вызов его через срабатывание скрипта посредством действия скрипта «Торговля с персонажем». Это действие, в свою очередь, вызывает функцию cApp::SetupBarter, которая настраивает информацию, необходимую функции BarterFrame. Эта информация включает персонаж, который продает предметы, а также имя файла системы управления имуществом персонажа (ICS):

BOOL cApp::SetupBarter(sCharacter *Character, char *ICSFilename)
{
    g_BarterChar = Character;
    strcpy(g_BarterICS, ICSFilename);
    m_StateManager.Push(BarterFrame, this);

    return TRUE;
}

Функция состояния BarterFrame сканирует загруженную ICS, отображая каждый содержащийся в списке имущества персонажа предмет на экране. Если игрок щелкает по предмету, и у него достаточно денег, предмет покупается. Когда игрок завершает свои дела с лавочником, состояние торговли извлекается из стека состояний, и происходит возврат к игровому процессу.

Воспроизведение звуков и музыки

Во время игры воспроизводится музыка и другие звуки. Эти игровые звуки, несколько плоховатые (можете сказать, что я не мастер звукозаписи!), воспроизводятся вызовом PlaySound. Единственный аргумент PlaySound — индекс в массиве имен звуковых файлов, который вы объявляете в начале кода приложения:

// Глобальные имена файлов звуковых эффектов
#define NUM_SOUNDS 9

char *g_SoundFilenames[NUM_SOUNDS] = {
    { "..\\Data\\Attack1.wav" },
    { "..\\Data\\Attack2.wav" },
    { "..\\Data\\Spell.wav" },
    { "..\\Data\\Roar.wav" },
    { "..\\Data\\Hurt1.wav" },
    { "..\\Data\\Hurt2.wav" },
    { "..\\Data\\Die1.wav" },
    { "..\\Data\\Die2.wav" },
    { "..\\Data\\Beep.wav" }
};

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

BOOL cApp::PlaySound(long Num)
{
    if(Num >=0 && Num < NUM_SOUNDS) {
        m_SoundData.Free();
        if(m_SoundData.LoadWAV(g_SoundFilenames[Num]) == TRUE)
            m_SoundChannel.Play(&m_SoundData);

        return TRUE;
    }

    return FALSE;
}

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

char *g_MusicFilenames[] = {
    { "..\\Data\\Cathedral_Sunrise.mid" },
    { "..\\Data\\Distant_tribe.mid" },
    { "..\\Data\\Escape.mid" },
    { "..\\Data\\Jungle1.mid" },
    { "..\\Data\\Magic_Harp.mid" },
    { "..\\Data\\Medi_Strings.mid" },
    { "..\\Data\\Medi_techno.mid" },
    { "..\\Data\\Song_of_the_sea.mid" },
    { "..\\Data\\Storm.mid" }
};

Здесь нам не надо отслеживать номер песни (мы живем на дикой стороне!), поэтому можно сразу перейти к функции PlayMusic:

BOOL cApp::PlayMusic(long Num)
{
    // Не меняем песню, если она уже воспроизводится
    if(g_CurrentMusic == Num)
        return TRUE;

Перед продолжением работы вы проверяете, какая песня воспроизводится в данный момент. Глобальная переменная отслеживает последнюю воспроизводившуюся песню, и, если эта песня продолжает воспроизводиться, вам не надо запускать воспроизведение той же самой песни снова (продолжается воспроизведение текущей песни). Если воспроизводится новая песня, громкость плавно понижается до нуля, текущая песня освобождается, загружается новая песня и запускается воспроизведение музыки:

    // Останавливаем и освобождаем текущую песню
    m_MusicChannel.Stop();
    m_MusicChannel.Free();

    // Плавно понижаем громкость музыки, давая DirectMusic
    // достаточно времени для завершения последней песни
    // (иначе новая песня не будет воспроизводиться корректно).
    // Число 700 основывается на громкости воспроизведения музыки,
    // можете подстроить его
    DWORD Timer = timeGetTime() + 700;
    while(timeGetTime() < Timer) {
        DWORD Level = (Timer - timeGetTime()) / 10;
        m_MusicChannel.SetVolume(Level);
    }

    // Загружаем и воспроизводим новую песню
    m_MusicChannel.Load(g_MusicFilenames[Num]);
    m_MusicChannel.Play(70, 0);

    // Запоминаем номер новой песни
    g_CurrentMusic = Num;

    return TRUE;
}

Визуализация сцены

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

BOOL cApp::RenderFrame(long Elapsed)
{
    long i, j;

    // Визуализация упрощенной сетки для z-значений
    m_Graphics.EnableZBuffer(TRUE);
    m_SceneObject.Render();

    // Рисование фона (составленного из шести текстур)
    m_Graphics.EnableZBuffer(FALSE);
    m_Graphics.BeginSprite();
    for(i = 0; i < 2; i++) {
        for(j = 0; j < 3; j++)
            m_SceneTextures[i*3+j].Blit(j*256,i*256);
    }
    m_Graphics.EndSprite();

    // Рисование трехмерных объектов
    m_Graphics.EnableZBuffer(TRUE);
    m_CharController.Render(Elapsed);
    m_SpellController.Render();

    return TRUE;
}

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

Обработка скриптов

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

Пример использует класс скрипта и производный класс скрипта, разработанные в главе 12. В то время, как класс скрипта хранится в файлах script.h и script.cpp, используемую производную версию класса скрипта игра The Tower хранит в файлах game_script.h и game_script.cpp. Пропустив класс скрипта (поскольку он остался таким же, как в главе 12), исследуем производный класс скрипта с именем cGameScript:

class cGameScript : public cScript
{
  private:
    // Внутренние массивы переменных и флагов
    BOOL m_Flags[256];
    long m_Vars[256];

    // Родительский объект приложения
    cApp *m_App;

    // Текстовое окно для отображения сообщений
    cWindow m_Window;

Скрипты используют массивы флагов и переменных (m_Flags и m_Vars), оба массива содержат по 256 элементов. Некоторые действия скриптов используют эти флаги и переменные для хранения данных и выполнения проверок условий с целью управления потоком обработки скрипта. Также сохраняется указатель на экземпляр класса приложения (для вызова функций приложения), и создается объект текстового окна для отображения диалогов персонажей и других текстов.

Затем в классе cGameScript вы определяете объект sRoutePoint, используемый скриптами для конструирования и назначения маршрутов персонажам:

    // Маршрутная точка для конструирования маршрута персонажа
    long m_NumRoutePoints;
    sRoutePoint *m_Route;

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

    // Стандартные действия обработки
    sScript *Script_End(sScript*);
    sScript *Script_Else(sScript*);
    sScript *Script_EndIf(sScript*);
    sScript *Script_IfFlagThen(sScript*);
    sScript *Script_IfVarThen(sScript*);
    sScript *Script_SetFlag(sScript*);
    sScript *Script_SetVar(sScript*);
    sScript *Script_Label(sScript*);
    sScript *Script_Goto(sScript*);
    sScript *Script_Message(sScript*);

    // Действия, связанные с персонажами
    sScript *Script_Add(sScript*);
    sScript *Script_Remove(sScript*);
    sScript *Script_Move(sScript*);
    sScript *Script_Direction(sScript*);
    sScript *Script_Type(sScript*);
    sScript *Script_AI(sScript*);
    sScript *Script_Target(sScript*);
    sScript *Script_NoTarget(sScript*);
    sScript *Script_Bounds(sScript*);
    sScript *Script_Distance(sScript*);
    sScript *Script_Script(sScript*);
    sScript *Script_CharMessage(sScript*);
    sScript *Script_Enable(sScript*);
    sScript *Script_CreateRoute(sScript*);
    sScript *Script_AddPoint(sScript*);
    sScript *Script_AssignRoute(sScript*);
    sScript *Script_AlterHPMP(sScript*);
    sScript *Script_Ailment(sScript*);
    sScript *Script_AlterSpell(sScript*);
    sScript *Script_Teleport(sScript*);
    sScript *Script_ShortMessage(sScript*);
    sScript *Script_Action(sScript*);
    sScript *Script_IfExpLevel(sScript*);

    // Действия торговли/обмена
    sScript *Script_Barter(sScript*);

    // Действия, связанные с предметами
    sScript *Script_IfItem(sScript*);
    sScript *Script_AddItem(sScript*);
    sScript *Script_RemoveItem(sScript*);

    // Действия, связанные с барьерами
    sScript *Script_AddBarrier(sScript*);
    sScript *Script_EnableBarrier(sScript*);
    sScript *Script_RemoveBarrier(sScript*);

    // Действия, связанные с триггерами
    sScript *Script_AddTrigger(sScript*);
    sScript *Script_EnableTrigger(sScript*);
    sScript *Script_RemoveTrigger(sScript*);

    // Действия, связанные со звуком
    sScript *Script_Sound(sScript*);
    sScript *Script_Music(sScript*);
    sScript *Script_StopMusic(sScript*);

    // Действие завершения игры
    sScript *Script_WinGame(sScript*);

    // Действия комментариев и разделителей
    sScript *Script_CommentOrSeparator(sScript*);

    // Действие ожидания
    sScript *Script_Wait(sScript*);

    // Генерация случайных чисел
    sScript *Script_IfRandThen(sScript*);

    // Принудительная визуализация кадра
    sScript *Script_Render(sScript*);

Ух! Как много функций — и все они, как я сказал, непосредственно относятся к действиям скрипта. К счастью, функции обработки действий скрипта короткие и простые. В главе 10 были представлены скрипты, а в главе 12 представлен класс скрипта, так что обращайтесь в случае необходимости к этим главам, а сейчас вновь сосредоточимся на объявлении cGameScript:

    // Функция обработки if/then
    sScript *Script_IfThen(sScript *ScriptPtr, BOOL Skip);

Со всеми, относящимися к if...then функциями в шаблоне действий, проще разработать единую функцию, которая будет иметь дело с условной обработкой. Эта функция (Script_IfThen) получает указатель на следующее действие скрипта после действия if...then и флаг, определяющий состояние условия. Если Skip установлен в TRUE, все последующие действия скрипта пропускаются, пока не будет найдено действие скрипта Else или EndIf, в то время как, если Skip установлен в FALSE, условие считается выполненным, и все действия скрипта обрабатываются, пока не будет найдено действие скрипта Else или EndIf. Заметьте, что действие скрипта Else меняет состояние флага SkipTRUE на FALSE и наоборот), обеспечивая правильную обработку конструкции if...then...else.

Объявление cGameScript завершается еще двумя закрытыми функциями — первая, Release, вызывается для освобождения внутренних данных скрипта, каждый раз когда завершается обработка скрипта. Вторая функция, Process, содержит большую инструкцию switch, отправляющую обрабатываемые действия скрипта соответствующим им функциям (точно также, как было показано в главе 12):

    // Перегруженные функции обработки
    BOOL Release();
    sScript *Process(sScript *Script);

  public:
    cGameScript();
    ~cGameScript();

    BOOL SetData(cApp *App);
    BOOL Reset();
    BOOL Save(char *Filename);
    BOOL Load(char *Filename);
};

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

Вы вставляете объявление экземпляра производного класса скрипта cGameScript в объявление cApp для его использования главным приложением:

cGameScript m_Script;

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

Собираем части

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

Исследование проекта игры я рекомендую начать с файлов WinMain.h и WinMain.cpp; эти файлы содержат класс приложения, который формирует каркас приложения. Как подчеркивалось в этой главе, вы можете следовать потоку исполнения программы, от инициализации до завершения.


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

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