Творец. День пятый. Каин и Авель

«Эй вы, молокососы! Знаете ли вы, что такое настоящий порох? Знаете ли вы истинный его запах и цвет? Любите ли вы TNT так, как люблю его я?» – с этими словами полковник John McGee на глазах у рядовых солдат совершил суицид и подорвал подводную лодку тротилом в 15-тонном эквиваленте.

Американская культура добавила в наш быт жвачку, джинсы и боевики. Это американцы переврали всю историю человечества, начав её такими словами: «Авраам убил Исаака; Исаак убил Иакова; Иаков убил Иуду и братьев его». Любовь к немотивированному насилию крепко засела у простых обывателей и не слезет ещё лет пятьсот. Насилие добралось до всех СМИ, а в наш век просочилась и в самую эффективную отрасль массового психологического влияния на людей – компьютерные игры.

Open Кровь
Перерыв между предыдущей и нынешней статьями составил три недели. За это время я подвергался множеству моральных и аморальных сомнений, когда подошёл, собственно, к основе всех шутеров. Убийство. Они убивают – мы убиваем. Они плохие – мы хорошие. Они пытаются убить нас – мы убиваем их, только с целью сохранения своей жизни, и не более того.
Тестом общественного сознания стала игра Postal. Сами разработчики заявили после релиза, что игру можно пройти без единого убийства. Но у заядлых игроков рука так и тянулась к лопате, а лопата ложилась на плечи горожан. Но, к примеру, GTA мы никогда не пройдём, не пролив кровь. Да и сама игра выпячивала своё «достоинство» – это мы плохие, это мы нигга, это мы продаём оружие и нюхаем дурь…

TNT
Ну что ж, видимо, как и всем разработчикам, нам придётся закрыть глаза на этот маленький нюанс и просто решить интересную задачку. По крайней мере, я ввёл в курс дела о моральной ответственности каждого из игроделов.
Итак, три недели brainstorm не прошли даром – я разработал алгоритм «стреляния» для нашего платформера. Шлифовать его до блеска или оставить так – дело ваше. Единственное, что нужно отметить, – мы переходим на новый уровень программирования, который вы, может быть, так никогда и не постигли бы, занимаясь разработкой «блокнотов» и «интернетэксплореров». А именно: нам придётся затронуть основы высшей математики и немного поиграться с векторами.

Основы «вышки»
Моё образование размером в 10 классов не даёт мне с лёгкостью кидаться терминами и формулами, взятыми у студента политеха. За сим предлагаю переложить всё на плечи GLScene и использовать его стандартные процедуры и функции работы с векторами. Но сначала – немного теории.
По теории, пуля должна иметь вектор движения. Начало этого вектора находится на модели игрока, а сам вектор идёт по лучу, проходящему через курсор. То есть, куда игрок смотрит – туда и стреляет (насчёт игрока – чуть позже, пока разберёмся с пулями). Итак, каждый раз, когда пользователь щёлкает левой кнопкой мыши, мы быстренько создаём вектор движения, создаём пулю, присваиваем ей этот вектор и отправляем её в дальнее плавание.
Добавим в раздел var следующие переменные:

time: single; // время полёта пули
List: TStrings; // список всех пуль
BCount: integer; // счётчик пуль

Теперь в процедуре создания Form1 организуем список типа strings:

List:=TStringList.Create;

В GLSceneEditor добавим новые объекты: два DummyCube с именами Bullets и Attack, а также потомков для Player: LightSource с именем Light и Sprite под названием ShootFlash.
Направляемся в GLCadencer1Progress. Добавляем локальные переменные:

A, B: TVector; // векторы для направления движения пули
i: integer; // счётчик создания пуль
Bullet: TGLSphere; // переменная, содержащая текущую пулю

В теле процедуры пишем (полный код ищите в файловом архиве нашего форума):

Light.Shining:=False;
ShootFlash.Visible:=False;
time:=time+deltatime;

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

for i:=0 to List.Count-1 do
//удаляем все пули из списка
bullets.FindChild(list[i],true).Free;
//очищаем список
list.clear;
for i:=0 to bullets.Count-1 do
 begin
  //продолжаем движение пуль
  bullets.Children[i].Move(deltatime*200);
  bullets.Children[i].Position.Z:=0;
 if bullets.Children[i].DistanceTo(where)>GLCamera1.DepthOfView
  then
  //добавляем в List пули, которые уже не видны
  list.Add(bullets.Children[i].Name);
 end;

Теперь пора создать новую пулю, если пользователь щёлкает левой кнопкой мыши:

if (IsKeyDown(VK_LBUTTON)) and (time>0.02) then
 begin
  if time<1 then
   begin
    ShootFlash.Visible:=True;
    Light.Shining:=True;
   end else
   begin
    //Включаем вспышку и сразу убираем
    Light.Shining:=False;
    ShootFlash.Visible:=False;
   end;
  time:=0.0; // сбрасываем время
 Bullet:=TGLSphere(Bullets.AddNewChild(TGLSphere));
  with Bullet do
   begin
     // создаём новую пулю, уменьшаем её и присваиваем имя
    Scale.X:=0.1; Scale.Y:=0.1;
    Scale.Z:=0.1;
    Name:=’Bullet’+IntToStr(BCount);
   end;

Теперь переходим к работе с векторами. Для начала укажем направление вектора и занесём значение в соответсвующую переменную:

 A:=Attack.AbsoluteDirection;
 ScaleVector(A, 1);

Затем создаём длину вектора, задав начало и конец (точнее наоборот):

 B:=VectorSubtract(Attack.AbsolutePosition, Player.AbsolutePosition);

Теперь присваиваем эти векторные переменные нашей пуле:

Bullet.AbsoluteDirection:= VectorAdd(B, A);
Bullet. AbsolutePosition:=Player.AbsolutePosition;

Вторая строчка – это положение пули в начале её движения.
Наконец, даём нашей пуле хорошенького пинка:

Bullet.Move(deltatime*200);

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

Bullet.Visible:=False;

Теперь пришло время поработать со столк-новениями. За любые столкновения объектов в GLScene отвечает GLCollisionManager. Проще говоря, каждый раз, когда какие-нибудь объекты задевают друг друга, то сразу возникает событие CollisionManagerCollision, в которое передаются имена столкнувшихся объектов. Чтобы добавить пули в список объектов, которые проверяются на столкновение, надо добавить после строки

Bullet:=TGLSphere(Bullets.AddNewChild(TGLSphere));

следующий код:

with GetOrCreateCollision(Bullet) do
 begin
  BoundingMode:=cbmSphere;
  Manager:=CollisionManager1;
 end;

Предварительно, конечно же, нам надо добавить на форму компонент GLCollisionManager. Никаких особых свойств, кроме имени он не имеет, поэтому оставляем всё как есть. Перейдите во вкладку Events компонента коллизий и щёлкните два раза по строчке OnCollision. У нас появилась новая процедура, про которую я уже рассказывал. Добавьте локальные переменные:

// пуля и объект, в который она попала
Obj, Bullet: TGLBaseSceneObject;
// точка и нормаль точки, куда попала пуля
Point, Normal: TVector;

Нам нужно узнать, если среди столкнувшихся объектов объект с именем Bullet.

if pos('Bullet', object1.Name)=1 then
 // если первый объект – пуля, то
 begin
  (* Присваиваем переменным имена
     столкнувшихся объектов *)
  Obj:=object2;
  Bullet:=object1;
 end
 else
   // если второй объект пуля, то
  if pos(’Bullet’,object2.Name)=1 then
   begin
    (* то же самое, что и в первом
    условии, только другой порядок *)
    Obj:=object1;
    Bullet:=object2;
   end
  else exit;

Если ни один из столкнувшихся объектов не является пулей, то сразу выходим из события. Далее пишем:

Obj.RayCastIntersect(Bullet.Position.AsVector,Bullet.Direction.AsVector,@Point,@Normal);

Эта процедура для программистов среднего уровня почти необъяснима (для меня, если честно, до конца тоже непонятна). Если переводить дословно, то её название звучит так: «Бросок Луча Пересекается». То есть, говоря простым языком, мы ищем точку, в которой вектор пули пересекается с объектом. Теперь пишем следующее:

(* создаём новый объект-плоскость небольшого размера, на который мы можем нанести, к примеру, текстуру дырки *)
with TGLPlane(World.AddNewChild(TGLPlane)) do
 begin
  // немного выпячиваем точку над поверхностью объекта
 Point[1]:=Point[1]+0.1;
 // Ставим «дырку» вместо нашей точки Point
 AbsolutePosition:=Point;
 // корректируем нормаль
 AbsoluteDirection:=Normal;
 Width:=1; Height:=1;
 // загружаем текстуру
 Material.Texture.LoadFromFile(‘имя_ текстуры’);
end;
(* пуля, которая была задействована, попадает в наш список для удаления *)
List.Add(Bullet.Name);

…он играет, как умеет
Ещё один кирпичик нашей игры сделан. Пожалуй, мы разобрались со всеми основными моментами игры. Но если вы внимательно читаете заголовки статей цикла, то наверняка заметили, что текущая статья идёт под пятым днём. В оставшиеся «дни» я расскажу вам о дополнительных возможностях 3D-графики GLScene. А потом…

Продолжение следует...

Рекомендуем почитать: