Создание 3D игр на C++
Урок 9
Препятствия


Итак, в прошлом уроке мы сделали первый шаг к воплощению игры: создали карту и прошлись по ней.
Но вы заметили, что в текущем состоянии мы ходим так, будто бы ввели в консоли Quake строку NoClip. Иначе говоря, наш игрок спокойно проходит сквозь стены, что неправильно. Сегодня мы впервые столкнемся с математической частью игры. Нам предстоит попытаться предусмотреть столкновение со стеной и не позволить пройти сквозь нее.

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

Итак, начнем. Скорее всего, работа с векторами - не первая математическая проблема, с которой вам предстоит столкнуться. Чаще всего решать их непосредственно в теле проекта бывает проблематично по многим причинам. А потому рекомендую вам сразу создать консольное приложение
File->New->Console Wizard
В этом окошке снимите использование VCL, она нам не понадобится.
Теперь надо очистить предложенный BCB исходный код. Дальше необходимо подгрузить три заголовочных файла:
iostream.h - для вывода на экран наших действий.
stdio.h - только из-за функции getc, чтоб поместить ее вконце кода.
math.h - для математических функций. Нам понадобится только sqrt.
После заголовочных файлов начнем описывать наш вектор. Для начала создадим тело класса:
class MyVector
{
private:
public:
};
Приватных свойств и методов у нас не будет. Так что эту область можно сразу удалить. Дальше объявим три переменных, которые, собственно и составляют наш вектор:
   float x, y, z;
Довольно часто надо знать размер вектора, а потому нам нужно как-то его возвращать средстваи класса. Можно было бы написать метод, который каждый раз бы вычислял размер вектора, но это было бы накладно по времени, если бы нам пришлось несколько раз обращаться к размеру без изменений вектора. А потому размер мы будем хранить в переменной:
   float size;
А для того, чтобы обновлять эту переменную, создадим функцию:
   void RefreshSize () {size = sqrt (x*x+y*y+z*z); };
Ее надо будет вызывать каждый раз, когда нам надо узнать размер вектора, но мы не уверены, что он не был изменен с момента последнего вызова функции.
Теперь займемся непосредственно операциями над векторами. Первая из них - это сложение. Осуществить ее очень просто. Надо просто просуммировать все составляющие слагаемых векторов и вернуть результат:
   MyVector operator + (MyVector C)
   {
      MyVector A = C;
      A.x += x;
      A.y += y;
      A.z += z;
      return A;
   };
Аналогично пишется вычитание. Но тут надо учитывать, что вычитание - некоммутативная фунциция, то есть зависящая от порядка операндов. Нам необходимо строго учитывать вычитаемое и уменьшаемое:
   MyVector operator - (MyVector C)
   {
      MyVector A = *this;
      A.x -= C.x;
      A.y -= C.y;
      A.z -= C.z;
      return A;
   };
*this - это текущий вектор.
Дальше идет умножение вектора на скаляр. Это, опять же, некоммутативная функция, и ее придется объявить как friend. Формула очень проста: умножаем каждую составляющую на скалярную величину:
   friend MyVector operator * (float Scalar, MyVector Vector)
   {
      MyVector C;
      C.x = Vector.x * Scalar;
      C.y = Vector.y * Scalar;
      C.z = Vector.z * Scalar;
      return C;
   };
Теперь - скалярное и векторное произведение. Вот формулы их вычисления:

http://morgeyz.narod.ru/subscribe/vector.jpg
Итак, скалярное произведение. Его воплотить очень просто:
   float operator & (MyVector C) { return C.x*x + C.y*y + C.z*z; };
И последнее и самое сложное - это векторное произведение. Выглядит оно вот так:
   friend MyVector operator ^ (MyVector A, MyVector B)
   {
      MyVector C;
      C.x = A.y*B.z - A.z*B.y;
      C.y = A.z*B.x - A.x*B.z;
      C.z = A.x*B.y - A.y*B.x;
      return C;
   };
Эта функция так же некоммутативна. Векторное произведение возвращает ненормализованный перпендикуляр к плоскости, образованной двумя передаваемыми векторами (напомню: нормализованный вектор - это вектор, длинна которого равна нулю. Векторное произведение возвращает ненормализованный вектор).
Все, на этом с операторами покончено. Теперь добавим два конструктора:
   MyVector () {x = 0; y = 0; z = 0; size = 0; };
   MyVector (float X, float Y, float Z)
   {
      x = X; y = Y; z = Z;
      RefreshSize ();
   };
И класс готов. В конце концов должно было получиться что-то вроде этого:
class MyVector
{
private:
public:
   float x, y, z;
   float size;
   void RefreshSize () {size = sqrt (x*x+y*y+z*z); };
   friend MyVector operator * (float Scalar, MyVector Vector)
   {
      MyVector C;
      C.x = Vector.x * Scalar;
      C.y = Vector.y * Scalar;
      C.z = Vector.z * Scalar;
      return C;
   };
   MyVector operator + (MyVector C)
   {
      MyVector A = C;
      A.x += x;
      A.y += y;
      A.z += z;
      return A;
   };
   MyVector operator - (MyVector C)
   {
      MyVector A = *this;
      A.x -= C.x;
      A.y -= C.y;
      A.z -= C.z;
      return A;
   };
   float operator & (MyVector C) { return C.x*x + C.y*y + C.z*z; };
   friend MyVector operator ^ (MyVector A, MyVector B)
   {
      MyVector C;
      C.x = A.y*B.z - A.z*B.y;
      C.y = A.z*B.x - A.x*B.z;
      C.z = A.x*B.y - A.y*B.x;
      return C;
   };

   MyVector () {x = 0; y = 0; z = 0; size = 0; };
   MyVector (float X, float Y, float Z)
   {
      x = X; y = Y; z = Z;
      RefreshSize ();
   };

};
Чтобы проверить, насколько это все правильно, напишем функцию, которая будем выводить вектор на экран:
   void writeV (MyVector Vector)
   {
      cout << Vector.x << ", " << Vector.y << ", " << Vector.z << "\n";
   };
И такую вот проверку:
void main()
{
   MyVector Vect (5, 5, 5);
   writeV (Vect);
   cout <<  "Assigning" << "\n";
   Vect = MyVector (6, 6, 6);
   writeV (Vect);
   cout <<  "Addition, Subtraction" << "\n";
   Vect = Vect + MyVector (10, 7, 5);
   writeV (Vect);
   Vect = MyVector (20, 20, 20) - Vect;
   writeV (Vect);
   cout <<  "Multiplication by scalar" << "\n";
   Vect = MyVector (5, 5, 5);
   writeV (Vect);
   Vect = 10 * Vect;
   writeV (Vect);
   cout <<  "Dot Product, Cross Product" << "\n";
   Vect = MyVector (5, 5, 5);
   writeV (Vect);
   cout << float (Vect & MyVector(5, 5, 0)) << "\n";
   Vect = MyVector (5, 5, 5);
   writeV (Vect);
   Vect = Vect ^ MyVector (5, 0, 0);
   writeV (Vect);


   getc (stdin);

   return;
}

В результате запуска такой программы на экран должно выводиться следующее:
5, 5, 5
Assigning
6, 6, 6
Addition, Subtraction
16, 13, 11
4, 7, 9
Multiplication by scalar
5, 5, 5
50, 50, 50
Dot Product, Cross Product
5, 5, 5
50
5, 5, 5
0, 25, -25
Исходник готовый лежит тут, хотя я сомневаюсь, чтобы у вас что-то не получилось:
http://morgeyz.narod.ru/subscribe/vector.cpp
Все, с векторами закончили.

Теперь разберемся, что есть игрок в этой задаче и что - препятствие. Рассматривать игрока, как точку - это глупо. Как мы помним, при изменении координат положения игрока они изменяются в зависимости от времени, прошедшего с предыдущего шага игры. На медленной машине это время может быть чересчур велико, и игрок просто проскочит сквозь стену (то есть в один момент находиться с одной ее стороны, в другой - с другой). Предусмотреть этот момент можно, взяв игрока не как точку, а как отрезок, стягивающий его новую позицию с позицией до очередного шага.
Теперь что такое препятствие. В нашем конкретном примере это четырехугольные стены, всегда строго перпендикулярные полу. Но сейчас мы разрабатываем всего лишь простенький пример. На самом деле препятствие может иметь и более сложную форму. Нам надо это предусмотреть.
Известный факт, что любой многоугольник в пространстве можно представить множеством треугольников. Этим мы и воспользуемся. Т.о. теперь препятствие для нас - это треугольник.
Теперь, казалось бы, все просто: найдем точку пересечения и все. Но не тут-то было. Мало найти точку пересечения с самим препятствием. Ведь наш игрок не должен подойти к нему настолько, чтобы можно было видеть, что за этим препятствием. Как мы помним, расстяоние от камеры до ближайшей точки у нас равно 1. Но, при повороте на 45°, расстояние от самого левого края экрана до камеры будет sqrt (2) * 4 / 3. Это около 1,9. Будем считать, что никто не может подойти к стене ближе, чем на 2. Узнать, наткнулся ли игрок на препятствие, можно очень просто: найти точку пересечения прямой игрока с треугольником препятствия, смещенным на нормализованный вектор нормали этого самого препятствия (это только звучит дико. Если вчитаться, то все понятно, если вы, конечно, знакомы с геометрией). Нормализованный вектор - это вектор, параллельный текущему, длина которого равна 1. То есть, нам надо найти точку пересечения с препятствием, за которое нам действительно нельзя выходить, а не с тем, которое мы видим.
Вектора нормали мы нормализем, когда загружаем карту.
      // Normals' normalizing
      n = sqrt (Obj[i].xn * Obj[i].xn + Obj[i].yn * Obj[i].yn);
      Obj[i].xn = Obj[i].xn / n;
      Obj[i].yn = Obj[i].yn / n;
Такие строки были. Их надо немного переправить:
      // Normals' normalizing
      n = sqrt (Obj[i].xn * Obj[i].xn + Obj[i].yn * Obj[i].yn + Obj[i].zn * Obj[i].zn);
      Obj[i].xn = Obj[i].xn / n;
      Obj[i].yn = Obj[i].yn / n;
      Obj[i].zn = Obj[i].zn / n;
Дело в том, что еще до прошлого урока я собирался сделать двумерное столкновение с препятствиями, а потому нормализация шла в двух осях. Теперь же надо во всех трех.
Ход нормализации очень прост: мы считаем длину вектора нормали, а потом проекцию на каждую ось делим на эту самую длину.

Теперь разберемся с тем, как найти точку пересечения стены с отрезком. В одной статье мне удалось найти хорошую функцию для поиска этих самых пересечений, которой мы и воспользуемся. Итак, откройте проект, который мы создали в начале урока (консольный проект) и допишите между функциями writeV и main еще две:
int f1_sgn(const float& k) // функция, вычисляющая знак числа.
{
  if( k > 0 ) return 1;
  if( k < 0 ) return -1;
  return 0;
}
Думаю, эта функция не нуждается в больших комментариях. Она просто возвращает знак числа, и будет нужна всего однажды. А теперь вторая функция:
bool Intersect (MyVector  v1, // вершины треугольника.
                MyVector  v2,
                MyVector  v3,
                MyVector  n,  // нормаль треугольника.
                MyVector  p1, // первый конец отрезка.
                MyVector  p2, // второй конец отрезка.
                MyVector& pc) // возвращаемая точка пересечения.
{
  n = MyVector (0, 0, 0) - n;
  // вычисляем расстояния между концами отрезка и плоскостью треугольника.
  float r1 = n & (p1 - v1);
  float r2 = n & (p2 - v1);
  // если оба конца отрезка лежат по одну сторону от плоскости, то отрезок
  // не пересекает треугольник.
  if( f1_sgn(r1) == f1_sgn(r2) ) return false;
  // вычисляем точку пересечения отрезка с плоскостью треугольника.
  MyVector ip = (p1 + ((-r1 / (r2 - r1) * (p2 - p1))));
  // проверяем, находится ли точка пересечения внутри треугольника.
  if( (((v2 - v1) ^ (ip - v1)) & n) < 0) return false;
  if( (((v3 - v2) ^ (ip - v2)) & n) < 0) return false;
  if( (((v1 - v3) ^ (ip - v3)) & n) < 0) return false;
  pc = ip; return true; 
}
Для которой мы и писали наш класс вектора со всеми функциями. Разве что сложение не пригодилось. Функция прокомментирована и для человека, знающего векторную алгебру, вполне должна быть понятна. Для остальных просто пусть будет фактом, что она работает. Для проверки предлагаю такую функцию main:
void main()
{
   MyVector _vect (0, 0, 0);
   if (Intersect (
      MyVector (0, 0, 0),
      MyVector (0, 10, 0),
      MyVector (0, 0, 10),
      MyVector (1, 0, 0),
      MyVector (5, 3, 3),
      MyVector (-5, 1, 1),
      _vect
             ); )   writeV (_vect);
      else cout << "There're no points of intersect";

   if (Intersect (
      MyVector (0, 0, 0),
      MyVector (0, 10, 0),
      MyVector (0, 0, 10),
      MyVector (1, 0, 0),
      MyVector (5, 3, 3),
      MyVector (0, 2, 2),
      _vect
             ); )   writeV (_vect);
      else cout << "There're no points of intersect";

   if (Intersect (
      MyVector (0, 0, 0),
      MyVector (0, 10, 0),
      MyVector (0, 0, 10),
      MyVector (1, 0, 0),
      MyVector (5, 3, 3),
      MyVector (2, 1, 1),
      _vect
             ); )   writeV (_vect);
      else cout << "There're no points of intersect";

   getc (stdin);

   return;
}
Как видим, функция замечательно работает. Конечно, пример не самый лучший. Зато его легко представить на бумаге и убедиться в правильности работы.
Теперь перенесем эту функцию в наш проект. Откройте проект нашей игры и создайте новый модуль: Vectors.cpp. Его мы не начинаем col_, поскольку еще не редко вам предстоит этот модуль включать в свои разработки.
Содержание модуля должно быть следующим:

Файл Vectors.h:
//---------------------------------------------------------------------------
// Vectors.h
// Работа с векторами
// © SHD-ALakazam [ shd@bk.ru ]
// http://morgeyz.narod.ru/
//---------------------------------------------------------------------------
#ifndef VectorsH
#define VectorsH

#include <math.h>
//---------------------------------------------------------------------------
class MyVector
{
private:
public:
   float x, y, z;
   float size;
   void RefreshSize () {size = sqrt (x*x+y*y+z*z); };
   friend MyVector operator * (float Scalar, MyVector Vector)
   {
      MyVector C;
      C.x = Vector.x * Scalar;
      C.y = Vector.y * Scalar;
      C.z = Vector.z * Scalar;
      return C;
   };
   MyVector operator + (MyVector C)
   {
      MyVector A = C;
      A.x += x;
      A.y += y;
      A.z += z;
      return A;
   };
   MyVector operator - (MyVector C)
   {
      MyVector A = *this;
      A.x -= C.x;
      A.y -= C.y;
      A.z -= C.z;
      return A;
   };
   float operator & (MyVector C) { return C.x*x + C.y*y + C.z*z; };
   friend MyVector operator ^ (MyVector A, MyVector B)
   {
      MyVector C;
      C.x = A.y*B.z - A.z*B.y;
      C.y = A.z*B.x - A.x*B.z;
      C.z = A.x*B.y - A.y*B.x;
      return C;
   };

   MyVector () {x = 0; y = 0; z = 0; size = 0; };
   MyVector (float X, float Y, float Z)
   {
      x = X; y = Y; z = Z;
      RefreshSize ();
   };

};

bool Intersect (MyVector  v1, 
                MyVector  v2,
                MyVector  v3,
                MyVector  n,  
                MyVector  p1, 
                MyVector  p2, 
                MyVector& pc); 
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------


Файл Vectors.cpp
//---------------------------------------------------------------------------
// Vectors.cpp
// Работа с векторами
// © SHD-ALakazam [ shd@bk.ru ]
// http://morgeyz.narod.ru/
//---------------------------------------------------------------------------
#pragma hdrstop
#include "Vectors.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)

int f1_sgn(const float& k) // функция, вычисляющая знак числа.
{
  if( k > 0 ) return 1;
  if( k < 0 ) return -1;
  return 0;
}

bool Intersect (MyVector  v1, // вершины треугольника.
                MyVector  v2,
                MyVector  v3,
                MyVector  n,  // нормаль треугольника.
                MyVector  p1, // первый конец отрезка.
                MyVector  p2, // второй конец отрезка.
                MyVector& pc) // возвращаемая точка пересечения.
{
  n = MyVector (0, 0, 0) - n;
  // вычисляем расстояния между концами отрезка и плоскостью треугольника.
  float r1 = n & (p1 - v1);
  float r2 = n & (p2 - v1);
  // если оба конца отрезка лежат по одну сторону от плоскости, то отрезок
  // не пересекает треугольник.
  if( f1_sgn(r1) == f1_sgn(r2) ) return false;
  // вычисляем точку пересечения отрезка с плоскостью треугольника.
  MyVector ip = (p1 + ((-r1 / (r2 - r1) * (p2 - p1))));
  // проверяем, находится ли точка пересечения внутри треугольника.
  if( (((v2 - v1) ^ (ip - v1)) & n) < 0) return false;
  if( (((v3 - v2) ^ (ip - v2)) & n) < 0) return false;
  if( (((v1 - v3) ^ (ip - v3)) & n) < 0) return false;
  pc = ip; return true;
}
//---------------------------------------------------------------------------

Дальше про саму обработку препятствий. Для этого вспомним наш цикл AppIdle. Как вы помните, в прошлом уроке у нас там было закомментировано две строки:
      for (int j = 0; j < ObjCount; j++)
         RecalcPoint (_Players[i].X, NewX, Obj[j].x1, Obj[j].x2, _Players[i].Y, NewY, Obj[j].y1, Obj[j].y2, _Players[i].Z, _Players[i].Z, Obj[j].z1, Obj[j].z2, Obj[j].xn, Obj[j].yn, Obj[j].zn, NewX, NewY, 2);
Теперь их надо раскомментировать.
Функции передаются все необходимые координаты, вектор нормали и минимальное расстояние объекта до препятствия.
Что мы ждем от функции RecalcPoint? Нам надо, чтобы она проверяла, не заскакиваем ли мы за стену, и если заскакиваем, то остановила нас. Для этого поступим следующим образом: во-первых, разобьем стену на два треугольника, так как написанная нами функция для проверки препятствий работает только с треугольниками. Дальше с каждым из них проделаем одинаковую последовательность действий:
1. Отдалим ее от текущего положения на единичный вектор нормали, умноженный на минимальное расстояние.
2. Проверим на наличие пересечений отрезка пути нашего персонажа с текущим проверяемым треугольником.
3. Если их нет - забудем. Иначе вычислим расстояние от конечной точки отерзка до сдвинутого треугольника и на это расстояние откинем персонажа.
Вот и все. Теперь попробуем это воплотить:
void RecalcPoint (float x1, float x2, float x3, float x4, float y1, float y2, float y3, float y4, float z1, float z2, float z3, float z4, float xn, float yn, float zn, float &rx, float &ry, float fDist)
{
   MyVector
      N  (xn, yn, zn),
      T1 (x3, y3, z3),
      T2 (x4, y4, z3),
      T3 (x4, y4, z4),
      T4 (x3, y3, z4),
      I1 (x1, y1, z1),
      I2 (x2, y2, z2),
      fResult;

   // Отдаляем стену
   T1 = T1 + fDist * N;
   T2 = T2 + fDist * N;
   T3 = T3 + fDist * N;
   T4 = T4 + fDist * N;

   float Dist;

   if (Intersect (T1, T2, T3, N, I1, I2, fResult))
   {
      Dist = (N & (I2 - T1)) - 0.00001;
      I2 = I2 - Dist * N;
   }

   if (Intersect (T3, T4, T1, N, I1, I2, fResult))
   {
      Dist = (N & (I2 - T1)) - 0.00001;
      I2 = I2 - Dist * N;
   }

   rx = I2.x;
   ry = I2.y;
}
Функция предельно проста. Отодвигаем все стены на передаваемую минимальную дистанцию, после чего разбиваем их на два треугольника и проверяем каждый поочереди. Если есть пересечения, то отталкиваем игрока так, чтоб он оказался на положенном ему расстоянии. Мы отталкиваем на 0,00001 дальше, чем необходимо, так как в противном случае уперевшись в стену мы не сможем из нее вырваться (попробуйте убрать это число и посмотрите на эффект).

Теперь запустите проект. Все работает, кроме... Почему-то можно пройти сквозь повернутые под углом стены. Ошибку найти можно в модуле col_map.cpp - вся беда в нормализации. Ведь у объекта вектор нормали хранится в переменной типа char, а у повернутых стен вектор нормали по x и y равен корню из двух пополам, что, ясное дело, округляется до 0. Потому и не работает обход препятствия. Исправить надо как определение объекта в модуле col_map.h:
struct object_t
{
   char x1, y1, z1;
   char x2, y2, z2;
   float xn, yn, zn;
   float w, h;
   char TexIndex;
};
Так и всю загрузку карты в col_map.cpp:
bool LoadMap (AnsiString FileName)
{
   AddString ("Loading " + FileName + "...", true);
   FILE *f;
   if ((f = fopen (FileName.c_str(), "r")) == NULL)
   {
      AddString ("Cannot load map " + FileName, true);
      return false;
   }

   // Reading Map Name
   char a = 'z';
   MapName = "";
   while (a != '\0')
   {
      fread (&a, 1, 1, f);
      if (a != 0) MapName += a;
   }

   CurrStr = 0;
   AddString (EXT_SEPARATOR, false);
   AddString (MapName, false);

   fread (&MapWidth, 1, 1, f);
   fread (&MapHeight, 1, 1, f);
   fread (&MapZ, 1, 1, f);

   fread (&MapGravity, 1, 1, f);

   fread (&MapSky, 1, 1, f);
   fread (&MapFloor, 1, 1, f);

   fread (&SkyDX, 1, 1, f);
   fread (&SkyDY, 1, 1, f);

   SkyX = 0; SkyY = 0;

   fread (&SPCount, 1, 1, f);
   for (int i = 0; i < SPCount; i++)
   {
      fread (&SPs[i].x, 1, 1, f);
      fread (&SPs[i].y, 1, 1, f);
   }

   float n = 0; char _x, _y, _z;
   fread (&ObjCount, 1, 1, f);
   for (int i = 0; i < ObjCount; i++)
   {
      fread (&Obj[i].x1, 1, 1, f);
      fread (&Obj[i].y1, 1, 1, f);
      fread (&Obj[i].z1, 1, 1, f);
      fread (&Obj[i].x2, 1, 1, f);
      fread (&Obj[i].y2, 1, 1, f);
      fread (&Obj[i].z2, 1, 1, f);
      fread (&_x, 1, 1, f);
      fread (&_y, 1, 1, f);
      fread (&_z, 1, 1, f);
      fread (&Obj[i].TexIndex, 1, 1, f);
      Obj[i].w = sqrt ((Obj[i].x2 - Obj[i].x1) * (Obj[i].x2 - Obj[i].x1) + (Obj[i].y2 - Obj[i].y1) * (Obj[i].y2 - Obj[i].y1));
      Obj[i].h = abs (Obj[i].z2 - Obj[i].z1);
      // Normals' normalizing
      Obj[i].xn = _x;
      Obj[i].yn = _y;
      Obj[i].zn = _z;
      n = sqrt (Obj[i].xn * Obj[i].xn + Obj[i].yn * Obj[i].yn + Obj[i].zn * Obj[i].zn);
      Obj[i].xn = Obj[i].xn / n;
      Obj[i].yn = Obj[i].yn / n;
      Obj[i].zn = Obj[i].zn / n;
   }

   fclose (f);

   AddString (EXT_SEPARATOR, true);
   return true;
}
Осталась одна проблема: можно пройти на границе стыка двух стен в центре уровня. Проблема вполне очевидна: раз мы отталкиваем стену на вектор нормали, то такие (отодвинутые) стены уже не соединяются и между ними можно пройти. Мы решим эту беду нечестным способом: изменим проверку на принадлежность точки треугольнику в функции Intersect:
bool Intersect (MyVector  v1, // вершины треугольника.
                MyVector  v2,
                MyVector  v3,
                MyVector  n,  // нормаль треугольника.
                MyVector  p1, // первый конец отрезка.
                MyVector  p2, // второй конец отрезка.
                MyVector& pc) // возвращаемая точка пересечения.
{
  n = MyVector (0, 0, 0) - n;
  // вычисляем расстояния между концами отрезка и плоскостью треугольника.
  float r1 = n & (p1 - v1);
  float r2 = n & (p2 - v1);
  // если оба конца отрезка лежат по одну сторону от плоскости, то отрезок
  // не пересекает треугольник.
  if( f1_sgn(r1) == f1_sgn(r2) ) return false;
  // вычисляем точку пересечения отрезка с плоскостью треугольника.
  MyVector ip = (p1 + ((-r1 / (r2 - r1) * (p2 - p1))));
  // проверяем, находится ли точка пересечения внутри треугольника.
  if( (((v2 - v1) ^ (ip - v1)) & n) < -5) return false;
  if( (((v3 - v2) ^ (ip - v2)) & n) < -5) return false;
  if( (((v1 - v3) ^ (ip - v3)) & n) < -5) return false;
  pc = ip; return true;
}
Теперь вместо 0 мы сравниваем с -5. Число подобрано методом математического тыка, повторюсь: это нечестное решение проблемы. К сожалению, другого пока я не знаю.

Ну и немного мелочей:
Добавьте в класс игрока новые переменные:

bool Dead, OnGround, Attack;

Dead - мертв ли игрок
OnGround - в воздухе он или на земле
Attack - стреляет ли он

И напишите в файле col_plr.cpp функцию:
void PlrBorn (int i)
{
       int k = random (SPCount);
      _Players[i].X = SPs[k].x;
      _Players[i].Y = SPs[k].y;
      _Players[i].Z = 0;
      _Players[i].Pitch = 0;
      _Players[i].Yaw = 0;
      _Players[i].DX = 0;
      _Players[i].DY = 0;
      _Players[i].DZ = 0;
      _Players[i].Health = 100;
      _Players[i].Armor = 0;
      _Players[i].Left = false;
      _Players[i].Right = false;
      _Players[i].Forward = false;
      _Players[i].Back = false;
      _Players[i].OnGround = true;
      _Players[i].Dead = false;
      _Players[i].Attack = false;
}
Объявите ее в файле col_plr.h:

void PlrBorn (int);

Это нам понадобится для возрождения игроков. Теперь в col_main вместо аналогичного кода просто вызовите функцию:
   else
   {
      LoadMap (fDir + "Data\\Maps\\Isengard.map");
      CurrPlr = 0;
      PlrCount = 1;
      PlrBorn (CurrPlr); // <= вот здесь
   }
Теперь про переменную OnGround (другие две будут потом обработаны):

В функции TypeString1 (col_con) доработаем функцию обработки прыжка:
   else if (AnsiLowerCase(_Values[0]) == "jump" && !_Players [CurrPlr].Dead && _Players [CurrPlr].OnGround)
   {
      _Players [CurrPlr].DZ = 10;
      _Players [CurrPlr].OnGround = false;
   }
   else if (AnsiLowerCase(_Values[0]) == "jump") ;
Если прыгаем, и при этом на земле, и не мертвы, то прыгаем, иначе ничего не делаем (;), чтобы не выползла надпись Unknown command.
А в функции AppIdle (col_main) вместо такой строки:

if (_Players[i].Z < 0) {_Players[i].Z = 0; _Players[i].DZ = 0;}

напишем:

if (_Players[i].Z < 0) {_Players[i].Z = 0; _Players[i].DZ = 0; _Players[i].OnGround = true; }

Ну и добавим в консоль пару команд, которые пригодятся потом:
   else if (AnsiLowerCase(_Values[0]) == "+attack" && _Players [CurrPlr].Dead)
      PlrBorn (CurrPlr);

   else if (AnsiLowerCase(_Values[0]) == "+attack") _Players[CurrPlr].Attack = true;
   else if (AnsiLowerCase(_Values[0]) == "-attack") _Players[CurrPlr].Attack = false;
Если атака нажата, когда мы мертвы, то возродить, иначе - стрелять.
Это все в TypeString1 (col_con). Если атакует мертвый человек, то возродить, иначе атаковать.

Теперь про обработку мыша. Для этого (если мы этого еще не делали) добавим две новые клавиши в col_input:
   _Keys [  0].Name = "leftmouse";
   _Keys [  1].Name = "rightmouse";

А в обработчики событий OnMouseUp и OnMouseDown у формы frmMain напишем такой код:
//==============================================================================
// Обработка мыша
void __fastcall TfrmMain::FormMouseDown(TObject *Sender,
      TMouseButton Button, TShiftState Shift, int X, int Y)
{
   if (Button == mbLeft) TypeString (_Keys[0].Action);
   else if (Button == mbRight) TypeString (_Keys[1].Action);
}
//---------------------------------------------------------------------------
void __fastcall TfrmMain::FormMouseUp(TObject *Sender, TMouseButton Button,
      TShiftState Shift, int X, int Y)
{
   int Key;
   if (Button == mbLeft) Key = 0;
   else if (Button == mbRight) Key = 1;

   if (_Keys[Key].Action [1] == '+')
   {
      AnsiString temp = _Keys[Key].Action;
      temp [1] = '-';
      TypeString (temp);
   }
}
Вот, пока так оставим. Наверняка в проекте появилось еще несколько мелочей, о которых я просто не помню. Так что лучше посмотрите на готовый код.
Проект в том виде, в каком он должен быть после сегодняшнего урока, выставлен тут:
issue9.zip