главная страница статьи файлы о сайте ссылки |
Георгий Мошкин О том, как создавать летающие пульки, говорилось в статье "Создание летающих пулек". Теперь рассмотрим некоторые другие вопросы, связанные с созданием 2D и 3D игр. Сначала затронем тему оптимизации. Чтобы алгоритм полёта пули работал быстрее, необходимо несколько видоизменить программу pulki.zip (10kb). Проблема состоит в том, что для каждой пули мы в каждом кадре считаем SIN и COS угла полёта. При большом количестве пуль это существенно скажется на FPS игры, всё будет "дёргаться". Вот эта часть кода: // если летит, то Но если пуля летит прямолинейно, то переменная 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); Обозначим приращение так: dx:=3*cos(phi*3.14/180); Тогда выражения для полёта пули переменятся: dx:=3*cos(phi*3.14/180); Здесь всё просто. Раз пуля не меняет направление, то PHI=const. То есть значение угла PHI постоянно. А раз угол один и тот же, то и косинусы с синусами будут константами. Таким образом, переменные dx и dy тоже константы. Для оптимизации достаточно посчитать dx и dy один раз, когда пуля только-только вылетает. Изменим описание пули. Раньше тип "пулька" выглядел так: type Tpulka=record Теперь мы заменим угол phi на приращения dx и dy: type Tpulka=record А теперь посмотрим, как изменится процесс выстрела. Раньше это выглядело так: temp:=false; - если нашли пулю, то будет
true Вместо того, чтобы записывать угол, под которым летит пуля, мы подсчитаем приращения заранее: pulki[i].phi:=igrok1.phi; меняем на pulki[i].dx:=3*cos(phi*3.14/180); и код выглядит так: temp:=false; - если нашли пулю, то будет
true Для перемещения теперь уже не потребуется каждый кадр подсчитывать значения SIN и COS, так как это было сделано один раз (при выстреле). Подсчитанные значения мы занесли в pulki[i].dx и pulki[i].dx. Ну а теперь реализуем код перемещения пули. Раньше этот код выглядел так: with pulki[i] do x:=x+3*cos(phi*3.14/180); А теперь вот так: with pulki[i] do x:=x+dx; В этом и заключается оптимизация. Две строчки выше - это оптимизированное перемещение пули. Но здесь возникает одна проблема: а вдруг в вашей игре пули могут лететь не только по прямой, но и отклоняться от своей траектории. Что делать в этом случае? Вернёмся к старому коду, посмотрим на него: with pulki[i] do x:=x+3*cos(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; А потом подсчитываем значения тригонометрических функций и заносим их в массивы: for i:=0 to 360 do sinmassiv[i]:=sin(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 Что такое dx, dy? Это приращение, которое каждый кадр прибавляется к координатам x, y. Смысл такой: dx, dy - это вектор. Вектор скорости пули. А так как формула S=V*t где S - путь, V - скорость, t - время, то x:=x+dx*1; Причём t=1, так как мы считаем его не в секундах, а в кадрах. За один кадр пуля пролетает вот столько. Сам вектор скорости выглядит так: (dx,dy). Например, если мы обозначим вектор положения пули за POZ, а скорость пули за SKOR, то векторно перемещение пули будет выглядеть так: POZ = POZ + SKOR, где POZ, SKOR - вектора. Эта формула подходит и для 3D пространства. Наример, если у вас есть скелетно анимированная модель, то вы всегда занаете вектор, соответствующий направлению оружия: napr.x:=... И знаете координату пушки в пространстве: pushka.x:=... Чтобы осуществить движение пули, достаточно сделать следующее: pulya.x:=pulya.x+pulya.napr.x; При выстреле мы задаём пуле начальное положение и вектор скорости: pulya.x:=pushka.x; Поэтому векторно всё понятно. И есть удобные библиотеки для работы с векторами, когда вместо трёх строчек pulya.x:=pulya.x+pulya.napr.x; можно писать одну 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 имеет координату пули Скорее всего эту проблему можно решить и другими способами, и ей я особо не занимался. Другой способ заключается в том, чтобы уменьшить скорость полёта пули. Дело в том, что в играх нужно применять контроль скорости процессора. Вы можете прочитать об этом в моих статьях. Из-за этого на медленных компьютерах пуля имеет более высокую скорость. За один кадр пуле приходется перескочить на большую величину. На быстрых компьютерах эта проблема исчезает. Но если вдуг что-то "затормозит", начнёт подгружаться с диска, то из-за этого замедления может возникнуть ошибка. Так что решение состоит в том, чтобы делать такую проверку, которая определит попадание со 100%-тной вероятностью Теперь коснёмся немного вопроса о спецэффектах. После того, как пуля попадёт в какой-нибудь объект, хорошо бы сделать круитой спецэффект. Например, пуля попала в бетонную стену. После этого мы делаем pulka[i].letit:=false. Для создания эффектов хорошо подойдёт какой-нибудь движок частиц (particle engine). Во время попадания у вас есть координата пули и вы в этой точке создаёте 30-40 частиц серого с начальными скоростями, перпендикулярными стене.Теперь после попадания у вас от стены будут отлетать частички. При попадании в металл хорошо бы придать партиклам (particles - частицы) красный, белый и ярко желтый цвет. То есть как бы искры отлетят. Если у вас какой-то супер-бластер, то можно сделать какие-нибудь синие частицы. Рассмотрим самонаводящуюся ракету. Когда ракета летит, то за собой она оставляет след. Для реализации этого эффекта вам достаточно создавать полупрозрачные партиклы серого в текущей координате ракеты. Если у вас хороший оптимизированный движок, то время существования частиц может быть достаточно большим. Благодаря наличию начальных скоростей и рассеиванию частиц дым от ракеты постепенно будет рассеиваться. То есть в том месте, где производилась стрельба, возникнет дымок. Ещё один хороший эффект - это следы от пуль. Для этого есть несколько решений. Например, определив точку пересечения пули со полигоном стены, в этом месте можно наложить маленький полигончик (поверх) с картинкой дырки от пули. В некоторых играх для этого сделан массив дырок. Так как массив имеет ограниченный размер, то дырки постепенно исчезают. Хотя это и не обязательно. Возможно применить некоторые ухищрения, благодаря которым ничего не будет исчезать, и существенно повысится реалистичность игры. |