Пули в 2-D и 3-D пространстве, проверка попадания, оптимизация и эффекты.
главная страница статьи файлы о сайте ссылки
Пули в 2-D и 3-D пространстве, проверка попадания, оптимизация и эффекты.

Георгий Мошкин
tmtlib@narod.ru

О том, как создавать летающие пульки, говорилось в статье "Создание летающих пулек". Теперь рассмотрим некоторые другие вопросы, связанные с созданием 2D и 3D игр.

Сначала затронем тему оптимизации. Чтобы алгоритм полёта пули работал быстрее, необходимо несколько видоизменить программу pulki.zip (10kb). Проблема состоит в том, что для каждой пули мы в каждом кадре считаем SIN и COS угла полёта. При большом количестве пуль это существенно скажется на FPS игры, всё будет "дёргаться". Вот эта часть кода:

// если летит, то
if (pulki[i].letit=true) then
begin

DrawPulka(pulki[i].x,pulki[i].y,pulki[i].phi); // рисуем
pulki[i].life:=pulki[i].life+1; // увеличиваем время полёта
with pulki[i] do x:=x+3*cos(phi*3.14/180);
with pulki[i] do y:=y+3*sin(phi*3.14/180);
end;

Но если пуля летит прямолинейно, то переменная pulki[i].phi не меняется. Например, PHI = 45 градусов. Косинус cos(45*3.14/180) не меняется. Кстати, почему cos(45*3.14/180), а не cos(45)? Дело в том, что функция COS в Delphi принимает аргумент в радианах. Домножение на *3.14/180 служит для перевода градусов в радианы.

Итак, PHI=45, и cos(45)=const. И пока пуля i летит, кадр за кадром будет выполняться следующий код:

with pulki[i] do x:=x+3*cos(phi*3.14/180);
with pulki[i] do y:=y+3*sin(phi*3.14/180);

Обозначим приращение так:

dx:=3*cos(phi*3.14/180);
dy:=3*sin(phi*3.14/180);

Тогда выражения для полёта пули переменятся:

dx:=3*cos(phi*3.14/180);
dy:=3*sin(phi*3.14/180);
with pulki[i] do x:=x+dx;
with pulki[i] do y:=y+dy;

Здесь всё просто. Раз пуля не меняет направление, то PHI=const. То есть значение угла PHI постоянно. А раз угол один и тот же, то и косинусы с синусами будут константами. Таким образом, переменные dx и dy тоже константы.

Для оптимизации достаточно посчитать dx и dy один раз, когда пуля только-только вылетает. Изменим описание пули. Раньше тип "пулька" выглядел так:

type Tpulka=record
x,y:single; // координаты
phi:single; // направление полёта пули
letit:boolean; // true - летит, false - уже не летит (не существует)
life:integer; // сколько прошло времени после выстрела
end;

Теперь мы заменим угол phi на приращения dx и dy:

type Tpulka=record
x,y:single; // координаты
dx,dy:single; // направление полёта пули
letit:boolean; // true - летит, false - уже не летит (не существует)
life:integer; // сколько прошло времени после выстрела
end;

А теперь посмотрим, как изменится процесс выстрела. Раньше это выглядело так:

temp:=false; - если нашли пулю, то будет true
if klavishi.probel=true then - нажат пробел
for i:=0 to 100 do - пройдёмся по всем пулям
begin
// если пулька не летит - значит свободна
if pulki[i].letit=false then
begin
задаём пульке направление,
совпадающее с координатами X,Y и направлением игрока (PHI):
pulki[i].x:=igrok1.x+3*cos(phi*3.14/180);
pulki[i].y:=igrok1.y+3*sin(phi*3.14/180);
pulki[i].phi:=igrok1.phi;
pulki[i].letit:=true; - пуля летит
pulki[i].life:=0; - пуля выпущена ТОЛЬКО ЧТО, то есть время равно НУЛЮ
temp:=true - пуля найдена, поиск можно прекратить
end;
if temp=true then break; // нашли пульку - можно выходить из цикла
end;

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

pulki[i].phi:=igrok1.phi;

меняем на

pulki[i].dx:=3*cos(phi*3.14/180);
pulki[i].dy:=3*sin(phi*3.14/180);

и код выглядит так:

temp:=false; - если нашли пулю, то будет true
if klavishi.probel=true then - нажат пробел
for i:=0 to 100 do - пройдёмся по всем пулям
begin
// если пулька не летит - значит свободна
if pulki[i].letit=false then
begin
задаём пульке направление,
совпадающее с координатами X,Y и направлением игрока (PHI):
pulki[i].x:=igrok1.x+3*cos(phi*3.14/180);
pulki[i].y:=igrok1.y+3*sin(phi*3.14/180);
pulki[i].dx:=3*cos(phi*3.14/180);
pulki[i].dy:=3*sin(phi*3.14/180);

pulki[i].letit:=true; - пуля летит
pulki[i].life:=0; - пуля выпущена ТОЛЬКО ЧТО, то есть время равно НУЛЮ
temp:=true - пуля найдена, поиск можно прекратить
end;
if temp=true then break; // нашли пульку - можно выходить из цикла
end;

Для перемещения теперь уже не потребуется каждый кадр подсчитывать значения SIN и COS, так как это было сделано один раз (при выстреле). Подсчитанные значения мы занесли в pulki[i].dx и pulki[i].dx. Ну а теперь реализуем код перемещения пули. Раньше этот код выглядел так:

with pulki[i] do x:=x+3*cos(phi*3.14/180);
with pulki[i] do y:=y+3*sin(phi*3.14/180);

А теперь вот так:

with pulki[i] do x:=x+dx;
with pulki[i] do y:=y+dy;

В этом и заключается оптимизация. Две строчки выше - это оптимизированное перемещение пули. Но здесь возникает одна проблема: а вдруг в вашей игре пули могут лететь не только по прямой, но и отклоняться от своей траектории. Что делать в этом случае? Вернёмся к старому коду, посмотрим на него:

with pulki[i] do x:=x+3*cos(phi*3.14/180);
with pulki[i] do y:=y+3*sin(phi*3.14/180);

Ведь если phi пули меняется во время полёта, то SIN и COS придётся рассчитывать каждый кадр. А если это не пули, а самонаводящиеся ракеты, то без изменения угла PHI вообще не обойтись! Что здесь можно оптимизировать? Неужели нет способа избавиться от медленного просчёта сотен СИНУСОВ и КОСИНУСОВ каждый кадр?

Способ такой есть! И он уже применяется с давних времён в играх, а больше всего - в демомейкинге. Если вы не знаете, что такое demomaking, то посетите сайты http://www.demoscene.ru и http://www.democoder.ru. На первом сайте вы сможете найти готовые красивые демки с потрясной графикой и звуком, а на втором - узнаете как делаются демки.

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

Метод заключается в следующем: мы создаём "кеш" значений SIN и COS. То есть мы просчитываем для основных углов занчения SIN и COS заранее (в самом начале программы) и заносим эти значения в массив. Вместо тригонометрических функций в самой игре или демке потом используют массив вместо функции.

Делается это так. сначала мы задаём массивы с кешем:

sinmassiv:array[0..360] of single;
cosmassiv:array[0..360] of single;

А потом подсчитываем значения тригонометрических функций и заносим их в массивы:

for i:=0 to 360 do sinmassiv[i]:=sin(i*3.14/180);
for i:=0 to 360 do cosmassiv[i]:=cos(i*3.14/180);

Теперь в нашей игре мы можем использовать вместо SIN и COS подсчитанные заранее значения, которые есть в массивах sinmassiv и cosmassiv.

Например, мы хотим подсчитать значение косинусы угла 135 градусов. Раньше это выглядело бы так:

a:=sin(135*3.14/180)

Намного быстрее использовать значение из кеша. Использовать кеш очень просто:

a:=sinmassiv[135]

Ну а если угол 15.4817? В этом случае имеет место число с точкой. Придётся округлить значение угла:

a:=sinmassiv[round(15.4817)]

Разумеется, здесь теряется точность. Ещё небольшой ньюанс заключается в том, что угол в вашей игре может меняться не только от 0 до 360, но и дальше. Например, это может быть -1. Или 376 градусов. В этом случае вы должны просто воспользоваться тем, что SIN и COS обладают периодичностью, т.е. через каждые 360 градусов происходит повторение значений. И поэтому:

sin(376)=sin(16)

а для -1 градуса:

sin(-1)=sin(359)

Итак, мы разобрались с вопросом оптимизации. Теперь перейдём к теме 2D/3D игр. Чтобы применить пули в 3D пространстве, достаточно хорошо разобраться в 2D. Посмотрим как мы задавали пулю в 2D:

type Tpulka=record
x,y:single; // координаты
dx,dy:single; // направление полёта пули
letit:boolean; // true - летит, false - уже не летит (не существует)
life:integer; // сколько прошло времени после выстрела
end;

Что такое dx, dy? Это приращение, которое каждый кадр прибавляется к координатам x, y. Смысл такой: dx, dy - это вектор. Вектор скорости пули. А так как формула

S=V*t

где S - путь, V - скорость, t - время, то

x:=x+dx*1;
y:=y+dy*1;

Причём t=1, так как мы считаем его не в секундах, а в кадрах. За один кадр пуля пролетает вот столько. Сам вектор скорости выглядит так: (dx,dy). Например, если мы обозначим вектор положения пули за POZ, а скорость пули за SKOR, то векторно перемещение пули будет выглядеть так:

POZ = POZ + SKOR,

где POZ, SKOR - вектора.

Эта формула подходит и для 3D пространства. Наример, если у вас есть скелетно анимированная модель, то вы всегда занаете вектор, соответствующий направлению оружия:

napr.x:=...
napr.y:=...
napr.z:=...

И знаете координату пушки в пространстве:

pushka.x:=...
pushka.y:=...
pushka.z:=...

Чтобы осуществить движение пули, достаточно сделать следующее:

pulya.x:=pulya.x+pulya.napr.x;
pulya.y:=pulya.y+pulya.napr.y;
pulya.z:=pulya.z+pulya.napr.z;

При выстреле мы задаём пуле начальное положение и вектор скорости:

pulya.x:=pushka.x;
pulya.y:=pushka.y;
pulya.z:=pushka.z;

pulya.napr.x:=napr.x;
pulya.napr.y:=napr.y;
pulya.napr.z:=napr.z;

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

pulya.x:=pulya.x+pulya.napr.x;
pulya.y:=pulya.y+pulya.napr.y;
pulya.z:=pulya.z+pulya.napr.z;

можно писать одну

pulya.AddVector(pulya.napr);

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

Как видите, реализация полёта пуль - это достаточно просто. Но при этом понимание этого вопроса сразу делает из вас крутого gamedeveloper-а. По сути вопрос о полёте пуль является одним из важнейших в плане геймдевелопмента. После того, как вы научились делать пульки и самонаводящиеся ракеты, вы сразу поймёте и некоторые другие вещи по разработке игр.

Теперь затронем вопрос о проверке попадания. Обычно проблема заключается в том, что если пуля слишком быстро летит, то она буквально перескочит через цель, не поразив её. Это происходит из-за того, что не срабатывает алгоритм проверки столкновений.

Например, пуля имела координату x=0, y=0. Скорость dx=100,dy=0. Через один кадр пуля окажется в точке с координатой x=0+100, y=0+0. Из-за этого мы не попадём в объект, который находится в координате x=50, y=0.

Итак, пуля вроде бы пролетела через объект, но шак был слишком большой. То есть приращение большое, а из-за этого collision detection просто не сработал. Что делать в этом случае? Например, можно проверять пересечение отрезка со сферой. Координаты отрезка задавать следующим образом:

точка1 имеет координату пули
точка 2 имеет координату пули плюс скороть.

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

Дело в том, что в играх нужно применять контроль скорости процессора. Вы можете прочитать об этом в моих статьях. Из-за этого на медленных компьютерах пуля имеет более высокую скорость. За один кадр пуле приходется перескочить на большую величину. На быстрых компьютерах эта проблема исчезает. Но если вдуг что-то "затормозит", начнёт подгружаться с диска, то из-за этого замедления может возникнуть ошибка.

Так что решение состоит в том, чтобы делать такую проверку, которая определит попадание со 100%-тной вероятностью

Теперь коснёмся немного вопроса о спецэффектах. После того, как пуля попадёт в какой-нибудь объект, хорошо бы сделать круитой спецэффект. Например, пуля попала в бетонную стену. После этого мы делаем pulka[i].letit:=false. Для создания эффектов хорошо подойдёт какой-нибудь движок частиц (particle engine). Во время попадания у вас есть координата пули и вы в этой точке создаёте 30-40 частиц серого с начальными скоростями, перпендикулярными стене.Теперь после попадания у вас от стены будут отлетать частички. При попадании в металл хорошо бы придать партиклам (particles - частицы) красный, белый и ярко желтый цвет. То есть как бы искры отлетят. Если у вас какой-то супер-бластер, то можно сделать какие-нибудь синие частицы.

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

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