Наша игра с Genesis3D

[Предыдущая часть]   [Следующая часть]

Часть 2 - Мы двигаемся!

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

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

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

Еще пару слов, прежде чем мы начнем: я тут наворочал груду кода, обеспечивающего более/менее плавное движение. То есть все это дело можно было сделать и проще, но я решил лучше дать вам это сразу, чем вы бы потом мучались и сами все придумывали. Вот :)

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

Как происходит движение в Genesis3D: сначала нам надо определиться куда мы собираемся идти - вперед/назад, влево/вправо или (Oh my God! He's flying!!) вверх/вниз. Самые проницательные сразу догадаются, что речь идет об осях координат, применительных к игроку. Правильно! Например мы захотели идти вперед. Вперед означает прямо от направления, куда смотрит игрок. Если игрок повернется, то "прямо" относительно мира будет совсем другое.
Итак, у нас есть матрица камеры XForm и 3D-вектор geVec3D In; Получаем это вектор с помощью функции

geXForm3d_GetIn(&XForm, &In);

Теперь мы перемещаем камеру в соответствии с эти вектором на какое-то количество пунктов:

geVec3d_AddScaled (&tempPos, &In, Offset, &tempPos1);

Мы переместились (вернее записали в матрицу камеры перемещение) из позиции tempPos в позицию tempPos1 на Offset пунктов по вектору In. Вот так происходит движение. Все просто, не правда ли? Но это еще не все. Дальше мы смотрим, а куда мы собственно говоря попали?

BOOL Result = geWorld_Collision(MyWorld, &c_ExtBox.Min, &c_ExtBox.Max, &tempPos, &tempPos1, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision1);

Вот эта самая функция служит для обработки столкновений. Описание всех ее параметров смотрите в документации Genesis3D SDK, а я только коротко скажу, что смысл ее в определении есть ли что-нибудь между позициями tempPos и tempPos1. Это можно представить как коробок спичек протягиваемый по нитке с одного конца на другой. Если "коробок" натыкается на препятствие, функция возвращает true. Таким коробком в Genesis служит так называемый Bounding Box. Это параллелепипед, окружающий объект, чье столкновение с другим мы определяем. Понятно, что объект должен полностью помещаться в свой Box. Для камеры он будет размером 1x1x1, так как по своей сути камера невидима, а вот для игрока он будет уже соответствующих размеров. Обратите внимание, что если какая-то часть объекта высунется из своего Box'а, то она уже не будет учитываться при обработке столкновений, так как сложно динамически изменять размеры Box'а. Этим объясняется столь любимое всеми "соединение в экстазе" моделей игроков со стенами практически в любой 3-rd person игре.
Мы остановились на том, что обнаружили столкновение в некой точке между tempPos и tempPos1. Что дальше?

if (Result==1) tempPos1=Collision1.Impact;

Это значит, что tempPost1 равна позиции столкновения и свой путь камера закончит у стенки, а не в стенке. Элементарно, не правда ли?!

Теперь все вместе. В классе GenCamera мы изменим функцию SetupViewXForm, которая теперь будет выглядеть так:

//Установить матрицу камеры
VOID GenCamera::SetupViewXForm(geFloat Offset, geWorld* MyWorld)
{
//Временные позиции камеры
geVec3d tempPos;
geVec3d tempPos1;
//Объект столкновения
GE_Collision Collision1;

tempPos=Pos; //Начальная позиция

//Перемещаем камеру и получаем конечную позицию
geXForm3d_SetIdentity(&XForm);
geXForm3d_RotateZ(&XForm, Angles.Z);
geXForm3d_RotateX(&XForm, Angles.X);
geXForm3d_RotateY(&XForm, Angles.Y);
geVec3d In;
geXForm3d_GetIn(&XForm, &In);
geVec3d_AddScaled (&tempPos, &In, -Offset, &tempPos1); //Камера следует за игроком, поэтому и -Offset

//Что у нас там со стенками? :)
BOOL Result = geWorld_Collision(MyWorld, &c_ExtBox.Min, &c_ExtBox.Max, &tempPos, &tempPos1,
GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision1);
if (Result==1) tempPos1=Collision1.Impact;

//Закрепляем конечную позицию
Pos=tempPos1;

//Записываем все перемещения в матрицу камеры
geXForm3d_Translate(&XForm, Pos.X, Pos.Y, Pos.Z);
geCamera_SetWorldSpaceXForm(Lens, &XForm);
}

Не забудьте также исправить определение функции в Camera.h и ее вызов в GenApp::RenderWorld() последнее выглядит так:

Camera->SetupViewXForm(0, GetWorld()); //Установить матрицу камеры


Осталось непонятным откуда берется Bounding Box под названием c_ExtBox. Пишем! В Camera.h суем определение

geExtBox c_ExtBox;

А в Camera.сpp в функцию Create пишем установки Box'а. Все вместе выглядит так:

//Создание камеры
BOOL GenCamera::Create(geFloat fov, long left, long top, long right, long bottom)
{
// сохранить прямоугольник "панорамы" камеры
Rect.Left = left;
Rect.Top = top;
Rect.Right = right;
Rect.Bottom = bottom;

//Устанавливаем Bounding Box вокруг камеры
c_ExtBox.Min.X = -1.0f;
c_ExtBox.Min.Y = -1.0f;
c_ExtBox.Min.Z = -1.0f;
c_ExtBox.Max.X = 1.0f;
c_ExtBox.Max.Y = 1.0;
c_ExtBox.Max.Z = 1.0f;

// создать объект камеры
Lens = geCamera_Create(fov, &Rect);
if (Lens) return true;
else return Cleanup();
}

Не торопитесь с компиляцией. Хоть с камерой мы и покончили, перед нами основная часть работы - класс GenPlayer!

Сперва надо добавить следующие объявления в Player.h

geVec3d m_Direction; // Векторы направлений
geExtBox m_ExtBox; // BOX для обработки столкновения
int m_EyeHeight; // Высота камеры на уровне глаз
geFloat m_Speed; // Скорость движения
geFloat m_GravitySpeed; // Скорость падения
geFloat m_JumpVelocity; // Стадия прыжка
geFloat UpDown;
int ForwardBack;
int LeftRight;
BOOL InAir;
geFloat m_InVelocity; // ускорения по осям
geFloat m_LeftVelocity; // используются для плавного движения

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

VOID MovePlayer();
VOID Move(int Reverse, geFloat Velocity);

VOID MoveForward();
VOID MoveBackward();
VOID MoveStrafeLeft();
VOID MoveStrafeRight();
VOID MoveInStopping();
VOID MoveLeftStopping();
VOID MoveJump();

Теперь переходим в Player.cpp и начинаем по порядку. При инициализации надо задать начальные значения для новых переменных. Все вместе выглядит так:

//Конструктор - инициализация
GenPlayer::GenPlayer()
{
//Установить начальную позицию игрока
SetNewPos(0,0,0);
SetOldPos(0,0,0);
SetAngles(0,0,0);

//Установить матрицу
geXForm3d_SetIdentity(&m_XForm);

m_EyeHeight = 140;
m_Speed = 14;
m_GravitySpeed = -25;
m_JumpVelocity = 0;
m_InVelocity = 0;
m_LeftVelocity = 0;
InAir = FALSE;
}

Помните про Bounding Box? Ведь нам надо обрабатывать столкновения игрока со всем чем попало, для него тоже нужен свой Box. Его определение происходит в функции Create

//Создание игрока
BOOL GenPlayer::Create(geWorld* World, int width, int height,geFloat MouseSensitivity)
{
//Пока просто запоминаем разные значения
SetWorld(World);
m_Screen.x=width;
m_Screen.y=height;
m_MouseSensitivity=MouseSensitivity;

//Bounding Box игрока
m_ExtBox.Min.X = -20.0f;
m_ExtBox.Min.Y = 0.0f;
m_ExtBox.Min.Z = -20.0f;

m_ExtBox.Max.X = 20.0f;
m_ExtBox.Max.Y = (geFloat)m_EyeHeight;
m_ExtBox.Max.Z = 20.0f;

return true;
}

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

//================================
// GenPlayer::MoveForward
// Обработка движения вперед
//================================

VOID GenPlayer::MoveForward()
{
geXForm3d_SetIdentity(&m_XForm);
geXForm3d_SetYRotation(&m_XForm, m_Angles.Y);
geXForm3d_GetIn(&m_XForm, &m_Direction);
if (!InAir) {
if (ForwardBack == -1) {
if (m_InVelocity > 0) m_InVelocity -= 4.0f;
else {m_InVelocity = 3.0f; ForwardBack = 1;}
}
else {
if (m_InVelocity <= m_Speed) m_InVelocity += 3.0f;
ForwardBack= 1;
}
}
else {
if (ForwardBack == 1) if (m_InVelocity <= m_Speed) m_InVelocity += 1.0f;
else {
if (m_InVelocity > 0) m_InVelocity -= 1.0f;
else {
if (m_InVelocity <= m_Speed) m_InVelocity += 0.5;
ForwardBack = 1;
}
}
}
Move(ForwardBack, m_InVelocity);
}

 


//====================================
// GenPlayer::MoveBackward
// Обработка движения назад
//====================================

VOID GenPlayer::MoveBackward()
{
geXForm3d_SetIdentity(&m_XForm);
geXForm3d_SetYRotation(&m_XForm, m_Angles.Y);
geXForm3d_GetIn(&m_XForm, &m_Direction);
if (!InAir) {
if (ForwardBack == 1) {
if (m_InVelocity > 0) m_InVelocity -= 4.0f;
else {m_InVelocity = 3.0f; ForwardBack = -1;}
}
else {
if (m_InVelocity <= m_Speed) m_InVelocity += 3.0f;
ForwardBack= -1;
}
}
else {
if (ForwardBack == -1) if (m_InVelocity <= m_Speed) m_InVelocity += 1.0f;
else {
if (m_InVelocity > 0) m_InVelocity -= 1.0f;
else {
if (m_InVelocity <= m_Speed) m_InVelocity += 0.5;
ForwardBack = -1;
}
}
}
Move(ForwardBack, m_InVelocity);
}


//======================================
// GenPlayer::InStopping
// Торможение после движения вперед/назад
//======================================

VOID GenPlayer::MoveInStopping()
{
geXForm3d_SetIdentity(&m_XForm);
geXForm3d_SetYRotation(&m_XForm, m_Angles.Y);
geXForm3d_GetIn(&m_XForm, &m_Direction);
if (!InAir) m_InVelocity -= 2.0f;
else m_InVelocity -=0.5;
Move(ForwardBack, m_InVelocity);
}

Смысл всего алгоритма в том, чтобы при нажатии на клавиши вперед/назад или их отпускании мы не мгновенно меняли вектор и ускорение, а делали это постепенно. Так же при отрыве ног от земли (InAir) мы должны уменьшать ускорение. То же самое происходит и для клавиш влево/вправо. Долой устаревший поворот - эти клавиши были рождены для стрейфа!!!


//========================================
// GenPlayer::MoveStrafeLeft
// Обработка движения влево
//========================================

VOID GenPlayer::MoveStrafeLeft()
{
geXForm3d_SetIdentity(&m_XForm);
geXForm3d_SetYRotation(&m_XForm, m_Angles.Y);
geXForm3d_GetLeft(&m_XForm, &m_Direction);
if (!InAir) {
if (LeftRight == -1) {
if (m_LeftVelocity > 0) m_LeftVelocity -= 4.0f;
else {m_LeftVelocity = 3.0f; LeftRight = 1;}
}
else {
if (m_LeftVelocity <= m_Speed) m_LeftVelocity += 3.0f;
LeftRight= 1;
}
}
else {
if (LeftRight == 1) if (m_InVelocity <= m_Speed) m_LeftVelocity += 1.0f;
else {
if (m_LeftVelocity > 0) m_LeftVelocity -= 1.0f;
else {
if (m_InVelocity <= m_Speed) m_LeftVelocity += 0.5;
LeftRight = 1;
}
}
}
Move(LeftRight, m_LeftVelocity);
}

 

//=========================================
// GenPlayer::MoveStrafeRight
// Обработка движения вправо
//=========================================

VOID GenPlayer::MoveStrafeRight()
{
geXForm3d_SetIdentity(&m_XForm);
geXForm3d_SetYRotation(&m_XForm, m_Angles.Y);
geXForm3d_GetLeft(&m_XForm, &m_Direction);
if (!InAir) {
if (LeftRight == 1) {
if (m_LeftVelocity > 0) m_LeftVelocity -= 4.0f;
else {m_LeftVelocity = 3.0f; LeftRight = -1;}
}
else {
if (m_LeftVelocity <= m_Speed) m_LeftVelocity += 3.0f;
LeftRight= -1;
}
}
else {
if (LeftRight == -1) if (m_InVelocity <= m_Speed) m_LeftVelocity += 1.0f;
else {
if (m_LeftVelocity > 0) m_LeftVelocity -= 1.0f;
else {
if (m_InVelocity <= m_Speed) m_LeftVelocity += 0.5;
LeftRight = -1;
}
}
}
Move(LeftRight, m_LeftVelocity);
}


VOID GenPlayer::MoveLeftStopping()
{
geXForm3d_SetIdentity(&m_XForm);
geXForm3d_SetYRotation(&m_XForm, m_Angles.Y);
geXForm3d_GetLeft(&m_XForm, &m_Direction);
m_LeftVelocity -= 2.0f;
Move(LeftRight, m_LeftVelocity);
}

А вот движение по вертикали происходит немного по другому. На нас ведь давит сила гравитации, не правда ли? У нас она будет обозначена неким ускорением, постоянно давящим вниз. Естественно, что если под ногами что-то есть, то мы всегда будем находиться в точке столкновения с полом. Ну а ежели нет... То извините... :0)
Здесь же обработаем прыжок. Если нажат пробел, то мы сообщаем моментальное ускорение, действующее вверх, затем, пока мы не коснемся земли, мы уменьшаем это ускорение. Пока мы в InAir, мы не можем сильно влиять на положение по горизонтали и тем более не можем снова прыгать.


//=========================================
// GenPlayer::MoveJump
// Прыжок, а по совместительству и наложение гравитации
//=========================================

VOID GenPlayer::MoveJump()
{
GE_Collision Collision;
geVec3d tempPos;
geVec3d tempPos1;
BOOL Result;


geXForm3d_SetIdentity(&m_XForm);
geXForm3d_SetYRotation(&m_XForm, m_Angles.Y);
tempPos = m_NewPos;
tempPos1=tempPos;
//Движение вверх/вниз
UpDown = m_GravitySpeed + m_JumpVelocity;
if (m_JumpVelocity != 0) m_JumpVelocity -= 2.0f;
geXForm3d_GetUp(&m_XForm, &m_Direction);
geVec3d_AddScaled (&tempPos, &m_Direction, UpDown, &tempPos1);
Result = geWorld_Collision(GetWorld(), &m_ExtBox.Min, &m_ExtBox.Max, &tempPos, &tempPos1,
GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);
if (Result == 1)
{
if (UpDown > 0) m_JumpVelocity= -m_GravitySpeed;
if (UpDown < 0) {InAir = false; m_JumpVelocity=0;}
tempPos1 = Collision.Impact;
}
else InAir=true;

m_NewPos = tempPos1;
}

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


VOID GenPlayer::Move(int Reverse, geFloat Velocity)
{
GE_Collision Collision;
geVec3d tempPos;
geVec3d tempPos1;
geFloat Slide;

tempPos = m_NewPos;
geVec3d_AddScaled (&tempPos, &m_Direction, Velocity * Reverse, &tempPos1);
BOOL Result = geWorld_Collision(GetWorld(), &m_ExtBox.Min, &m_ExtBox.Max, &tempPos, &tempPos1,
GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL, 0, NULL, NULL, &Collision);

if (Result == 1)
{
Slide = geVec3d_DotProduct (&tempPos1, &Collision.Plane.Normal) - Collision.Plane.Dist;
tempPos1.X -= Collision.Plane.Normal.X * Slide;
tempPos1.Y -= Collision.Plane.Normal.Y * Slide;
tempPos1.Z -= Collision.Plane.Normal.Z * Slide;
if (geWorld_Collision(GetWorld(), &m_ExtBox.Min, &m_ExtBox.Max, &tempPos, &tempPos1, GE_CONTENTS_SOLID_CLIP, GE_COLLIDE_ALL,0,NULL,NULL,&Collision))
tempPos1 = Collision.Impact;
}
m_NewPos = tempPos1;
}

Ну и наконец процедура, которая в зависимости от нажатия и ненажатия клавиш управляет вызовами всех вышеописанных функций.

VOID GenPlayer::MovePlayer()
{
MoveHead();

if (IsKeyDown(VK_UP)) MoveForward();
else if (IsKeyDown(VK_DOWN)) MoveBackward();
else if (m_InVelocity > 0) MoveInStopping();
else m_InVelocity=0;

if (IsKeyDown(VK_LEFT)) MoveStrafeLeft();
else if (IsKeyDown(VK_RIGHT)) MoveStrafeRight();
else if (m_LeftVelocity > 0) MoveLeftStopping();
else m_LeftVelocity=0;

if (IsKeyDown(VK_SPACE)) if (m_JumpVelocity==0) m_JumpVelocity = 45.0f;
MoveJump();
}

Осталось добавить последний штрих: в файле main.cpp переписать функцию MainLoop() чтобы она, во-первых, вызывала не обработку движения головы, а обработку всех движений, а во-вторых, надо написать вызов функции GenCamera::SetPos чтобы позиция устанавливалась на уровне "глаз" игрока.
Вот полный вид функции:


//=======================================
// MainLoop - главный цикл программы
//=======================================

VOID MainLoop()
{
App->GetPlayer()->MovePlayer(); //Переместить игрока
//Установить камеру
App->GetCamera()->SetPos(App->GetPlayer()->m_NewPos.X,
App->GetPlayer()->m_NewPos.Y + App->GetPlayer()->m_EyeHeight,
App->GetPlayer()->m_NewPos.Z
);
App->GetCamera()->SetAngles(App->GetPlayer()->GetAngles());
//Рендерить мир
App->RenderWorld();
}

Ну вот, собственно говоря и все! Если я ничего не пропустил, то компилируйте проект, запускайте и делайте ваши первые шаги!

Вы можете загрузить готовый проект прямо сейчас (403k)

В следующей части мы добавим в программу модель игрока.

Приятного программирования, Antiloop

[Вверх]

Posted: 28.01.2k1
Autor: Antiloop
<anti_loop@mail.ru>