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

Работа над игровым клиентом

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

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

Для использования клиентского приложения (оно находится на прилагаемом к книге CD-ROM в каталоге \BookCode\Chap15\Client), выполните следующие действия:

  1. Найдите и запустите приложение Client. Появится диалоговое окно Connect to Server (рис. 15.14).


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

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


  2. В диалоговом окне Connect to Server введите IP-адрес главного узла, выберите адаптер и введите имя вашего игрока.

  3. Щелкните OK, чтобы начать игру и подключиться к серверу. В некоторых отношениях клиент работает почти идентично серверу и, в частности, это относится к работе с игроками.

Обработка данных игрока

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

// Состояния игрока
#define STATE_IDLE  1
#define STATE_MOVE  2
#define STATE_SWING 3
#define STATE_HURT  4

// Анимации
#define ANIM_IDLE  1
#define ANIM_WALK  2
#define ANIM_SWING 3
#define ANIM_HURT  4

typedef struct sPlayer {
    BOOL Connected;    // TRUE если игрок активен
    DPNID dpnidPlayer; // ID игрока в DirectPlay

    long State;        // Последнее известное состояние (STATE_*)

    long Time;         // Время последнего обновления состояния
    long Latency;      // Половина полного цикла запаздывания в мс

    float XPos, YPos, ZPos; // Координаты игрока
    float Direction;   // Угол направления взгляда
    float Speed;       // Скорость перемещения

    cObject Body;      // 3-D объект персонажа
    cObject Weapon;    // 3-D объект оружия

    long LastAnim;     // Последняя установленная анимация

    // Конструктор и деструктор
    sPlayer()
    {
        Connected = FALSE;
        dpnidPlayer = 0;
        LastAnim = 0;
        Time = 0;
    }

    ~sPlayer()
    {
        Body.Free();
        Weapon.Free();
    }
} sPlayer;

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

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

Также загружается список анимаций. Эти анимации представляют различные состояния игроков: анимация ходьбы, стояние (ожидание), размахивание оружием и, наконец, анимация ранения. Анимации устанавливаются функцией UpdatePlayers, которую вы увидите чуть позже в разделе «Обновление локального игрока».

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

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

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

Для перебора списка игроков клиент использует функцию с именем GetPlayerNum, возвращающую индекс соответствующего игрока в массиве (или –1, если совпадений не обнаружено):

long cApp::GetPlayerNum(DPNID dpnidPlayer)
{
    long i;

    // Контроль ошибок
    if(m_Players == NULL)
        return -1;

    // Поиск совпадения в списке
    for(i = 0; i < MAX_PLAYERS; i++) {
        if(m_Players[i].dpnidPlayer == dpnidPlayer &&
           m_Players[i].Connected   == TRUE)
            return i;
    }

    return -1; // В списке ничего не найдено
}

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

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

Сетевой компонент

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

Использование клиентского сетевого компонента начнем с наследования нашего собственного класса от cNetworkClient:

class cClient : public cNetworkClient
{
  private:
    BOOL ConnectComplete(DPNMSG_CONNECT_COMPLETE *Msg);
    BOOL Receive(DPNMSG_RECEIVE *Msg);
};

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

BOOL cClient::Receive(DPNMSG_RECEIVE *Msg)
{
    // Отправка сообщения экземпляру класса приложения
    // (если он есть) 
    if(g_Application != NULL)
        g_Application->Receive(Msg);

    return TRUE;
}

BOOL cApp::Receive(DPNMSG_RECEIVE *Msg)
{
    sMessage *MsgPtr;

    // Получаем указатель на полученные данные
    MsgPtr = (sMessage*)Msg->pReceiveData;

    // Обрабатываем пакеты в зависимости от их типа
    switch(MsgPtr->Header.Type) {
        case MSG_PLAYER_INFO:    // Добавляем игрока к списку
        case MSG_CREATE_PLAYER:
            CreatePlayer(MsgPtr);
            break;

        case MSG_DESTROY_PLAYER: // Удаляем игрока из списка
            DestroyPlayer(MsgPtr);
            break;

        case MSG_STATE_CHANGE:   // Меняем состояние игрока
            ChangeState(MsgPtr);
            break;
    }

    return TRUE;
}

Как видите, внутри функции cClient::Receive вы передаете сетевое сообщение в функцию cApp::Receive. В функции cApp::Receive вы обрабатываете четыре различных сообщения: MSG_PLAYER_INFO, MSG_CREATE_PLAYER, MSG_DESTROY_PLAYER и MSG_STATE_CHANGE.

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

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

cApp *g_Application; // Глобальный указатель на объект приложения

BOOL cClient::ConnectComplete(DPNMSG_CONNECT_COMPLETE *Msg)
{
    // Сохраняем флаг подключения
    if(Msg->hResultCode == S_OK)
        g_Connected = TRUE;
    else
        g_Connected = FALSE;

    // Получаем идентификатор игрока из кода завершения подключения
    if(g_Application != NULL)
        g_Application->SetLocalPlayerID(Msg->dpnidLocal);

    return TRUE;
}

Здесь есть кое-что интересное — вызов функции SetLocalPlayerID вашего класса приложения. Спрашиваете, что это за функция? Помните, как вы использовали функцию cApp::Receive для проталкивания сетевых сообщений в объект вашего приложения? То же можно сказать и о функции cApp::SetLocalPlayerID — она передает идентификационный номер локального игрока в класс приложения:

BOOL cApp::SetLocalPlayerID(DPNID dpnid)
{
    if(m_Players == NULL)
        return FALSE;

    EnterCriticalSection(&m_UpdateCS);
    m_Players[0].dpnidPlayer = dpnid;
    LeaveCriticalSection(&m_UpdateCS);

    return TRUE;
}

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

Обработка сообщений

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


Рис. 15.15. Клиент получает сообщения от клиентского сетевого компонента точно так же, как и сервер

Рис. 15.15. Клиент получает сообщения от клиентского сетевого компонента точно так же, как и сервер. Однако клиент сразу преобразовывает входящие сообщения в игровые и обрабатывает их


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

cApp::CreatePlayer

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

void cApp::CreatePlayer(sMessage *Msg)
{
    sCreatePlayerMessage *cpm;
    long PlayerNum, i;

    // Контроль ошибок
    if(m_Players == NULL || !m_Players[0].dpnidPlayer)
        return;

    // Получаем указатель на данные сообщения
    cpm = (sCreatePlayerMessage*)Msg;

    // Локального игрока добавлять в список не надо 
    if(cpm->Header.PlayerID == m_Players[0].dpnidPlayer)
        return;

    // Проверяем, что игрока нет в списке
    // и одновременно ищем свободный слот
    PlayerNum = -1;

    // Перебираем список, пропустив локального игрока(слот 0) 
    for(i = 1; i < MAX_PLAYERS; i++) {
        if(m_Players[i].Connected == TRUE) {
            if(m_Players[i].dpnidPlayer == cpm->Header.PlayerID)
                return;
        } else
            PlayerNum = i;
    }

    // Ошибка - нет свободных слотов
    if(PlayerNum == -1)
        return;

    // Начало критической секции
    EnterCriticalSection(&m_UpdateCS);

    // Добавляем данные игрока
    m_Players[PlayerNum].Connected = TRUE;
    m_Players[PlayerNum].dpnidPlayer = cpm->Header.PlayerID;
    m_Players[PlayerNum].XPos = cpm->XPos;
    m_Players[PlayerNum].YPos = cpm->YPos;
    m_Players[PlayerNum].ZPos = cpm->ZPos;
    m_Players[PlayerNum].Direction = cpm->Direction;
    m_Players[PlayerNum].Speed = 0.0f;
    m_Players[PlayerNum].State = STATE_IDLE;
    m_NumPlayers++;

    // Покидаем критическую секцию
    LeaveCriticalSection(&m_UpdateCS);
}

cApp::DestroyPlayer

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

void cApp::DestroyPlayer(sMessage *Msg)
{
    sDestroyPlayerMessage *dpm;
    long PlayerNum;

    // Контроль ошибок
    if(m_Players == NULL || !m_Players[0].dpnidPlayer)
        return;

    // Получаем указатель на данные сообщения
    dpm = (sDestroyPlayerMessage*)Msg;

    // Не удалять из списка локального игрока
    if(dpm->Header.PlayerID == m_Players[0].dpnidPlayer)
        return;

    // Получаем номер игрока в списке
    if((PlayerNum = GetPlayerNum(dpm->Header.PlayerID)) == -1)
        return;

    // Начало критической секции
    EnterCriticalSection(&m_UpdateCS);

    // Помечаем игрока как отключившегося
    m_Players[PlayerNum].Connected = FALSE;
    m_NumPlayers--;

    // Покидаем критическую секцию
    LeaveCriticalSection(&m_UpdateCS);
}

cApp::ChangeState

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

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

void cApp::ChangeState(sMessage *Msg)
{
    sStateChangeMessage *scm;
    sRequestPlayerInfoMessage rpim;
    long PlayerNum;

    // Контроль ошибок
    if(m_Players == NULL || !m_Players[0].dpnidPlayer)
        return;

    // Получаем указатель на данные сообщения
    scm = (sStateChangeMessage*)Msg;

    // Получаем номер игрока в списке
    if((PlayerNum = GetPlayerNum(scm->Header.PlayerID)) == -1) {

        // Неизвестный игрок - запрашиваем информацию
        if(PlayerNum == -1) {
            // Конструируем сообщение
            rpim.Header.Type = MSG_PLAYER_INFO;
            rpim.Header.Size = sizeof(sRequestPlayerInfoMessage);
            rpim.Header.PlayerID = m_Players[0].dpnidPlayer;
            rpim.PlayerID = scm->Header.PlayerID;

            // Отправляем сообщение серверу
            SendNetworkMessage(&rpim, DPNSEND_NOLOOPBACK);

            return;
        }
    }

    // Начало критической секции
    EnterCriticalSection(&m_UpdateCS);

    // Сохраняем информацию о новом состоянии
    m_Players[PlayerNum].Time = timeGetTime();
    m_Players[PlayerNum].State = scm->State;
    m_Players[PlayerNum].XPos = scm->XPos;
    m_Players[PlayerNum].YPos = scm->YPos;
    m_Players[PlayerNum].ZPos = scm->ZPos;
    m_Players[PlayerNum].Direction = scm->Direction;
    m_Players[PlayerNum].Speed = scm->Speed;
    m_Players[PlayerNum].Latency = scm->Latency;

    // Ограничиваем запаздывание одной секундой
    if(m_Players[PlayerNum].Latency > 1000)
        m_Players[PlayerNum].Latency = 1000;

    // Подстраиваем время в зависимости от запаздывания
    m_Players[PlayerNum].Time -= m_Players[PlayerNum].Latency;

    // Покидаем критическую секцию
    LeaveCriticalSection(&m_UpdateCS);
}

Также как у сервера, у клиента есть функция SendNetworkMessage для отправки относящихся к игре сетевых сообщений серверу (за дополнительными подробностями работы этой функции обратитесь к исходному коду клиента на прилагаемом к книге CD-ROM).

Обновление локального игрока

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

Для обновления локального игрока используется функция cApp::Frame. Для управления персонажами игроки используют клавиатуру и мышь, поэтому я добавил пару объектов ядра ввода (m_Keyboard и m_Mouse):

BOOL cApp::Frame()
{
    static DWORD UpdateCounter = timeGetTime();
    static long MoveAction = 0, LastMove = 0;
    static BOOL CamMoved = FALSE;
    BOOL AllowMovement;
    long Dir;

    float Angles[13] = { 0.0f, 0.0f,   1.57f, 0.785f, 3.14f,
                         0.0f, 2.355f, 0.0f,  4.71f,  5.495f,
                         0.0f, 0.0f,   3.925f };

    // Получаем в каждом кадре локальный ввод
    m_Keyboard.Acquire(TRUE);
    m_Mouse.Acquire(TRUE);
    m_Keyboard.Read();
    m_Mouse.Read();

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

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

    // Обработка экрана подключения
    if(g_Connected == FALSE || !m_Players[0].dpnidPlayer) {

        // Отображение сообщения о подключении
        m_Graphics.Clear();
        if(m_Graphics.BeginScene() == TRUE) {
            m_Font.Print("Connecting to server...", 0, 0);
            m_Graphics.EndScene();
        }

        m_Graphics.Display();
        return TRUE;
    }

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


Рис. 15.16. Каждый разряд в переменной MoveAction используется для определения конкретного действия

Рис. 15.16. Каждый разряд в переменной MoveAction используется для определения конкретного действия


Следующий код определяет, какие клавиши нажимает игрок в данный момент, и устанавливает соответствующие разряды переменной MoveAction:

    // Сохраняем перемещение в каждом кадре
    if(m_Keyboard.GetKeyState(KEY_UP) == TRUE)
        MoveAction |= 1;

    if(m_Keyboard.GetKeyState(KEY_RIGHT) == TRUE)
        MoveAction |= 2;

    if(m_Keyboard.GetKeyState(KEY_DOWN) == TRUE)
        MoveAction |= 4;

    if(m_Keyboard.GetKeyState(KEY_LEFT) == TRUE)
        MoveAction |= 8;

    // Сохраняем действие атаки
    if(m_Keyboard.GetKeyState(KEY_SPACE) == TRUE)
        MoveAction |= 16;

    if(m_Mouse.GetButtonState(MOUSE_LBUTTON) == TRUE)
        MoveAction |= 16;

    // Вращаем камеру
    if(m_Mouse.GetXDelta() > 0) {
        m_CamAngle -= 0.1f;
        CamMoved = TRUE;
    }
    if(m_Mouse.GetXDelta() < 0) {
        m_CamAngle += 0.1f;
        CamMoved = TRUE;
    }

    // Обновляем игрока каждые 33 мс (30 раз в секунду) 
    if(timeGetTime() < UpdateCounter + 33)
        return TRUE;

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

    // Устанавливаем флаг, разрешающий перемещение игрока 
    AllowMovement = TRUE;

    // Нельзя перемещаться, если мы размахиваем оружием
    if(m_Players[0].State == STATE_SWING)
        AllowMovement = FALSE;

    // Нельзя перемещаться, если мы подверглись нападению
    if(m_Players[0].State == STATE_HURT)
        AllowMovement = FALSE;

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

    // Обрабатываем перемещение, если разрешено
    if(AllowMovement == TRUE) {

        // Обработка атаки
        if(MoveAction & 16) {
            MoveAction = 0; // Очищаем перемещение
            LastMove = 0; // Очищаем последнее перемещение

            // Отправляем сообщение об атаке -
            // пусть сервер даст сигнал размахивать мечом 
            sStateChangeMessage Msg;

            Msg.Header.Type = MSG_STATE_CHANGE;
            Msg.Header.Size = sizeof(sStateChangeMessage);
            Msg.Header.PlayerID = m_Players[0].dpnidPlayer;
            Msg.State = STATE_SWING;
            Msg.Direction = m_Players[0].Direction;

            // Отправляем сообщение на сервер
            SendNetworkMessage(&Msg, DPNSEND_NOLOOPBACK);
        }

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

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

        // Обработка перемещений локального игрока
        if((Dir = MoveAction) > 0 && Dir < 13) {

            // Устанавливаем новое состояние игрока
            // (с временем и направлением) 
            EnterCriticalSection(&m_UpdateCS);
            m_Players[0].State = STATE_MOVE;
            m_Players[0].Direction = Angles[Dir] - m_CamAngle + 4.71f;
            LeaveCriticalSection(&m_UpdateCS);

            // Сбрасываем последнее перемещение,
            // если камера передвигалась с момента последнего обновления
            if(CamMoved  = TRUE) {
                CamMoved = FALSE;
                LastMove = 0;
            }

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

Теперь функция Frame определяет, изменил ли игрок направление перемещения (по сравнению с перемещением в последнем кадре):

            // Отправляем действие на сервер,
            // если последнее перемещение изменилось
            if(MoveAction != LastMove) {
                LastMove = MoveAction; // Сохраняем последнее действие
                m_Players[0].Time = timeGetTime();

                // Конструируем сообщение
                sStateChangeMessage Msg;

                Msg.Header.Type = MSG_STATE_CHANGE;
                Msg.Header.Size = sizeof(sStateChangeMessage);
                Msg.Header.PlayerID = m_Players[0].dpnidPlayer;
                Msg.State = STATE_MOVE;
                Msg.Direction = m_Players[0].Direction;

                // Отправляем сообщение на сервер
                SendNetworkMessage(&Msg, DPNSEND_NOLOOPBACK);
            }

Когда игрок двигается, клиент отправляет сообщение изменения состояния на сервер. Заметьте, что сообщение изменения состояния отправляется только если перемещение игрока отличается от последнего перемещения, которое он выполнял (записанного в переменной LastMove).

Если игрок не двигается, его состояние меняется на остановку (STATE_IDLE), и с помощью показанного ниже кода сообщение изменения состояния отправляется на сервер:

        } else {
            // Меняем состояние на ожидание
            EnterCriticalSection(&m_UpdateCS);
            m_Players[0].State = STATE_IDLE;
            LeaveCriticalSection(&m_UpdateCS);

            // Отправляем обновление только если во время
            // последнего обновления игрок двигался 
            if(LastMove) {
                LastMove = 0;

                sStateChangeMessage Msg;

                Msg.Header.Type = MSG_STATE_CHANGE;
                Msg.Header.Size = sizeof(sStateChangeMessage);
                Msg.Header.PlayerID = m_Players[0].dpnidPlayer;
                Msg.State = STATE_IDLE;
                Msg.Direction = m_Players[0].Direction;

                // Отправляем сообщение на сервер
                SendNetworkMessage(&Msg, DPNSEND_NOLOOPBACK);
            }
        }
    }

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

    // Обновляем всех игроков
    UpdatePlayers();

    // Визуализируем сцену
    RenderScene();

    MoveAction = 0; // Очищаем данные действий для следующего кадра
    UpdateCounter = timeGetTime(); // Сбрасываем счетчик обновлений

    return TRUE;
}

Обновление всех игроков

В то время как входные данные игрока обрабатываются в функции cApp::Frame, UpdatePlayers (вызов которой вы видели в коде из предыдущего раздела) обрабатывает игроков согласно соответствующим им состояниям.

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

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

void cApp::UpdatePlayers()
{
    long i;
    float XMove, ZMove, Dist, Speed;
    long Elapsed;

    // Обрабатываем перемещения всех активных игроков
    for(i = 0; i < MAX_PLAYERS; i++) {
        if(m_Players[i].Connected == TRUE) {

            // Получаем время, прошедшее с момента установки состояния
            Elapsed = timeGetTime() - m_Players[i].Time;

            // Обрабатываем состояние перемещения игрока
            if(m_Players[i].State == STATE_MOVE) {

                // Вычисляем дальность перемещения, основываясь
                // на прошедшем времени перемещения
                Speed = (float)Elapsed / 1000.0f * m_Players[i].Speed;
                XMove = (float)sin(m_Players[i].Direction) * Speed;
                ZMove = (float)cos(m_Players[i].Direction) * Speed;

                // Проверяем столкновения при перемещении -
                // нельзя продолжать перемещение, если что-то
                // блокирует путь 
                if(m_NodeTreeMesh.CheckIntersect(
                                    m_Players[i].XPos,
                                    m_Players[i].YPos + 16.0f,
                                    m_Players[i].ZPos,
                                    m_Players[i].XPos + XMove,
                                    m_Players[i].YPos + 16.0f,
                                    m_Players[i].ZPos + ZMove,
                                    &Dist) == TRUE)
                    XMove = ZMove = 0.0f;

                // Обновляем координаты
                EnterCriticalSection(&m_UpdateCS);
                m_Players[i].XPos += XMove;
                m_Players[i].YPos = 0.0f;
                m_Players[i].ZPos += ZMove;
                m_Players[i].Time = timeGetTime(); // Сброс времени
                LeaveCriticalSection(&m_UpdateCS);
            }

            // Установка новой анимации, если необходимо
            if(m_Players[i].State == STATE_IDLE) {
                if(m_Players[i].LastAnim != ANIM_IDLE) {
                    EnterCriticalSection(&m_UpdateCS);
                    m_Players[i].LastAnim = ANIM_IDLE;
                    m_Players[i].Body.SetAnimation( 
                         &m_CharacterAnim, "Idle", timeGetTime() / 32);
                    LeaveCriticalSection(&m_UpdateCS);
                }
            } else
                if(m_Players[i].State == STATE_MOVE) {
                    if(m_Players[i].LastAnim != ANIM_WALK) {
                        EnterCriticalSection(&m_UpdateCS);
                        m_Players[i].LastAnim = ANIM_WALK;
                        m_Players[i].Body.SetAnimation(
                             &m_CharacterAnim, "Walk", timeGetTime() / 32);
                        LeaveCriticalSection(&m_UpdateCS);
                    }
                } else
                    if(m_Players[i].State == STATE_SWING) {
                        if(m_Players[i].LastAnim != ANIM_SWING) {
                            EnterCriticalSection(&m_UpdateCS);
                            m_Players[i].LastAnim = ANIM_SWING;
                            m_Players[i].Body.SetAnimation( 
                                 &m_CharacterAnim, "Swing", timeGetTime() / 32);
                            LeaveCriticalSection(&m_UpdateCS);
                        }
                    } else
                        if(m_Players[i].State == STATE_HURT) {
                            if(m_Players[i].LastAnim != ANIM_HURT) {
                                EnterCriticalSection(&m_UpdateCS);
                                m_Players[i].LastAnim = ANIM_HURT;
                                m_Players[i].Body.SetAnimation(
                                     &m_CharacterAnim, "Hurt", timeGetTime() / 32);
                                LeaveCriticalSection(&m_UpdateCS);
                            }
                        }
        }
    }
}

Анимация персонажей обновляется только если она отличается от последней известной анимации. Отслеживает последнюю известную анимацию переменная sPlayer::LastAnim, а различные макроопределения ANIM_* определяют, какая анимация воспроизводится.

Клиент во всем блеске

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

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

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

Завершая описание клиентского приложения, упомяну о том, что вы будете иметь дело еще с несколькими модулями кода приложения, такими как выбор адаптера и подключение к серверу. Полный код вы найдете на прилагаемом к книге CD-ROM (загляните в папку \BookCode\Chap15\Client).


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

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