На прошлом уроке наша «красотка» научилась прыгать, пора научить её ещё и ходить. Игры, где игрок снаружи смотрит на главного героя, называют «от третьего лица». Я учил в школе понятия первого, второго и третьего лица применительно к русскому языку, но вот применительно к играм этого названия не понимаю. А где тогда «от второго лица»? Или «второе лицо» – это те бедняги, которых главный герой ежесекундно крошит в капусту? Но если их много, то как они все могут быть «вторым лицом»? А платформеры, в которых мы смотрим на главного героя сбоку – это «от третьего лица» или уже нет? В общем, думаю, что игры, в которых мы 99% времени смотрим на задницу главного героя или героини, вполне могут называться «от заднего лица» ☺, пусть нынешний урок так и называется.
Чтобы слишком не затягивать изложение урока, система движений у нас будет упрощённая с точки зрения программирования (в каком-то смысле даже «примитивная»). У нас есть пять видов движений: прыжок (его мы реализовали на прошлом уроке), ходьба вперёд, ходьба назад, поворот направо, поворот налево. В любой момент времени может выполняться только одно из этих движений. В реальной игре мы обычно можем совмещать их, но это несколько усложнило бы код, и для урока не требуется. Также в реальных играх камера обычно ведёт себя более сложным образом, чем простое висение ровно сзади, но это также вышло бы за рамки урока.
Во-первых, добавим несколько констант и глобальных переменных, теперь эта часть программы должна выглядеть так:
const float СКОРОСТЬ_ДВИЖЕНИЯ = 0.8; const float СКОРОСТЬ_ПОВОРОТА = 0.5; const int КАДРОВ_В_ПРЫЖКЕ = 60; const int КАДРОВ_В_ХОДЬБЕ = 46; СтандартныйОбработчикСобытий обработчик_событий; Место место_персонажа; Меш меш_персонажа; float номер_кадра = 1; bool прыжок = false; float движение = 0; float поворот = 0;
Значения констант со скоростями можно впоследствии поварьировать, но мне текущие значения показались подходящими. Как вы могли заметить по значению константы КАДРОВ_В_ХОДЬБЕ, в используемой нами анимации Ходьба2 присутствует 46 кадров (это два цикла ходьбы).
Во-вторых, давайте добавим ещё один светильник в сцену, а то у нас большая часть пространства для хождения осталась неосвещённой. Сейчас в функции сцена() у нас есть 4 строки, касающиеся объекта лампа0. Добавьте после них аналогичные 4 строки для объекта лампа1:
auto лампа1 = new Лампа("свет1", 1); лампа1.задать_диффузный_свет([1,1,1,1]); // Белый цвет auto место_лампы1 = менеджер.начальное_место.создать_ребёнка("Место лампы1", (Вектор3(2,2,-6)), Кватернион.НОЛЬ); место_лампы1.присоединить_объект(лампа1);
Как вариант для дальнейших улучшений, можно было бы оформить этот код отдельной функцией и вызывать её для каждой требуемой лампы.
Будем добавлять/править функции обработки нажатий клавиш:
поправим функцию нажатия пробела (усложняется условие в if):
bool обработка_отжатия_клавиши_пробел(SDL_Event e) { if ((!прыжок) && (движение == 0) && (поворот == 0)) { прыжок = true; номер_кадра = 1; } return false; }
добавим 4 функции поворотов:
bool обработка_нажатия_клавиши_вправо(SDL_Event e) { if ((!прыжок) && (движение == 0) && (поворот == 0)) { поворот = -1; } return false; } bool обработка_отжатия_клавиши_вправо(SDL_Event e) { if (поворот < 0) { поворот = 0; } return false; } bool обработка_нажатия_клавиши_влево(SDL_Event e) { if ((!прыжок) && (движение == 0) && (поворот == 0)) { поворот = 1; } return false; } bool обработка_отжатия_клавиши_влево(SDL_Event e) { if (поворот > 0) { поворот = 0; } return false; }
и добавим 4 функции движения:
bool обработка_нажатия_клавиши_вперёд(SDL_Event e) { if ((!прыжок) && (движение == 0) && (поворот == 0)) { движение = 1; номер_кадра = 1; } return false; } bool обработка_отжатия_клавиши_вперёд(SDL_Event e) { if (движение > 0) { движение = 0; номер_кадра = 1; } return false; } bool обработка_нажатия_клавиши_назад(SDL_Event e) { if ((!прыжок) && (движение == 0) && (поворот == 0)) { движение = -1; номер_кадра = КАДРОВ_В_ХОДЬБЕ; } return false; } bool обработка_отжатия_клавиши_назад(SDL_Event e) { if (движение < 0) { движение = 0; номер_кадра = 1; } return false; }
Возможны различные способы уменьшения количества этих функций (например, посмотрите на код класса ОбработчикСобытийСДвижением в модуле ddd.zavisimost.sobytija_SDL2), но, как я уже писал вначале, тут мы всё делаем «по-простому».
Нужно зарегистрировать все эти функции. Обработка клавиши Пробел у нас уже зарегистрирована в конце функции сцена(), для регистрации остальных добавьте там же эти строки:
обработчик_событий.зарегистрировать_функцию2(SDL_KEYDOWN, SDLK_UP, toDelegate(&обработка_нажатия_клавиши_вперёд)); обработчик_событий.зарегистрировать_функцию2(SDL_KEYUP, SDLK_UP, toDelegate(&обработка_отжатия_клавиши_вперёд)); обработчик_событий.зарегистрировать_функцию2(SDL_KEYDOWN, SDLK_DOWN, toDelegate(&обработка_нажатия_клавиши_назад)); обработчик_событий.зарегистрировать_функцию2(SDL_KEYUP, SDLK_DOWN, toDelegate(&обработка_отжатия_клавиши_назад)); обработчик_событий.зарегистрировать_функцию2(SDL_KEYDOWN, SDLK_RIGHT, toDelegate(&обработка_нажатия_клавиши_вправо)); обработчик_событий.зарегистрировать_функцию2(SDL_KEYUP, SDLK_RIGHT, toDelegate(&обработка_отжатия_клавиши_вправо)); обработчик_событий.зарегистрировать_функцию2(SDL_KEYDOWN, SDLK_LEFT, toDelegate(&обработка_нажатия_клавиши_влево)); обработчик_событий.зарегистрировать_функцию2(SDL_KEYUP, SDLK_LEFT, toDelegate(&обработка_отжатия_клавиши_влево));
Осталось доработать функцию обновления сцены. Добавьте эти два блока else if перед блоком else:
обработка поворота:
else if (поворот != 0) {
место_персонажа.поворот(длительность_кадра * поворот * СКОРОСТЬ_ПОВОРОТА);
}
обработка движения:
else if (движение != 0) { номер_кадра += длительность_кадра * движение * 25; меш_персонажа.установить_состояние_анимации("Ходьба2", номер_кадра); if (номер_кадра > КАДРОВ_В_ХОДЬБЕ) номер_кадра = 1; if (номер_кадра < 1) номер_кадра = КАДРОВ_В_ХОДЬБЕ; float сдвиг_z = длительность_кадра * движение * СКОРОСТЬ_ДВИЖЕНИЯ; Вектор3 сдвиг1 = Вектор3(0, 0, сдвиг_z); Вектор3 сдвиг2 = место_персонажа.получить_ориентацию() * сдвиг1; место_персонажа.сдвинуть(сдвиг2); }
Здесь требуется пояснить по поводу трёх последних строк, связанных с векторами сдвиг1 и сдвиг2. Если бы мы просто меняли координату Z (использовали бы только вектор сдвиг1), то наша ассасинша ходила бы только по одной прямой, и неважно, куда бы она при этом смотрела. В строке, где вычисляется сдвиг2, мы умножаем кватернион ориентации Места персонажа на этот вектор сдвига, и, таким образом, получаем вектор сдвига в нужную нам сторону.
Нужно привязать камеру к объекту место_персонажа, чтобы она следовала за ним. Удалите или закомментируйте 2-ю строку функции сцена(), где мы в прошлом уроке задавали позицию камеры. Вместо неё добавьте следующие три строки перед блоком if:
место_персонажа.добавить_ребёнка(менеджер.место_камеры); менеджер.место_камеры.задать_позицию(0,1.5,-3); // Камера "висит" в 3 метрах сзади на высоте полутора метров менеджер.место_камеры.задать_ориентацию(Кватернион(0,0,1,0)); // Камера повёрнута назад на 180 градусов
Собственно, на этом всё. Компилируйте и запускайте программу. Если всё сделано правильно, то «Кленовый лист» должна ходить и поворачиваться при нажатии клавиш со стрелками, прыгать при нажатии пробела, а камера должна следовать за ней.
Конечно же, если выйти за границу пола, персонаж девушки никуда не упадёт, она так же продолжит ходить по (теперь уже воображаемой) плоскости. Чтобы персонаж «падал» с обрывов или «упирался» в стены, как это и должно быть в хорошей игре, к этому коду требуется ещё много чего добавить. Кроме того, всё что связано с мешем, скелетом и движениями персонажа, возможно, было бы правильнее оформить в отдельном модуле в виде класса.