[Предыдущая часть] [Следующая часть]
Приветствую
снова! В прошлой части мы инициализировали движок, загружали уровень, смотрели
на него изнутри и даже могли вертеть головой во все стороны :) Настало время
побродить по нашему созданному миру.
Естественно, остается в силе все то, о чем я говорил в прошлой статье, то есть вы должны правильно установить движок, пути к библиотечным файлам и конфигурацию проекта 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>