← к содержанию

Урок 17. С видом «от заднего лица»

На прошлом уроке наша «красотка» научилась прыгать, пора научить её ещё и ходить. Игры, где игрок снаружи смотрит на главного героя, называют «от третьего лица». Я учил в школе понятия первого, второго и третьего лица применительно к русскому языку, но вот применительно к играм этого названия не понимаю. А где тогда «от второго лица»? Или «второе лицо» – это те бедняги, которых главный герой ежесекундно крошит в капусту? Но если их много, то как они все могут быть «вторым лицом»? А платформеры, в которых мы смотрим на главного героя сбоку – это «от третьего лица» или уже нет? В общем, думаю, что игры, в которых мы 99% времени смотрим на задницу главного героя или героини, вполне могут называться «от заднего лица» , пусть нынешний урок так и называется.

Чтобы слишком не затягивать изложение урока, система движений у нас будет упрощённая с точки зрения программирования (в каком-то смысле даже «примитивная»). У нас есть пять видов движений: прыжок (его мы реализовали на прошлом уроке), ходьба вперёд, ходьба назад, поворот направо, поворот налево. В любой момент времени может выполняться только одно из этих движений. В реальной игре мы обычно можем совмещать их, но это несколько усложнило бы код, и для урока не требуется. Также в реальных играх камера обычно ведёт себя более сложным образом, чем простое висение ровно сзади, но это также вышло бы за рамки урока.

  1. Во-первых, добавим несколько констант и глобальных переменных, теперь эта часть программы должна выглядеть так:

const float СКОРОСТЬ_ДВИЖЕНИЯ = 0.8;
const float СКОРОСТЬ_ПОВОРОТА = 0.5;
const int КАДРОВ_В_ПРЫЖКЕ = 60;
const int КАДРОВ_В_ХОДЬБЕ = 46;

СтандартныйОбработчикСобытий обработчик_событий;
Место место_персонажа;
Меш меш_персонажа;
float номер_кадра = 1;
bool прыжок = false;
float движение = 0;
float поворот = 0;

Значения констант со скоростями можно впоследствии поварьировать, но мне текущие значения показались подходящими. Как вы могли заметить по значению константы КАДРОВ_В_ХОДЬБЕ, в используемой нами анимации Ходьба2 присутствует 46 кадров (это два цикла ходьбы).

  1. Во-вторых, давайте добавим ещё один светильник в сцену, а то у нас большая часть пространства для хождения осталась неосвещённой. Сейчас в функции сцена() у нас есть 4 строки, касающиеся объекта лампа0. Добавьте после них аналогичные 4 строки для объекта лампа1:

    auto лампа1 = new Лампа("свет1", 1);
    лампа1.задать_диффузный_свет([1,1,1,1]);   // Белый цвет
    auto место_лампы1 = менеджер.начальное_место.создать_ребёнка("Место лампы1", 
                                                       (Вектор3(2,2,-6)),
                                                       Кватернион.НОЛЬ);
    место_лампы1.присоединить_объект(лампа1);

Как вариант для дальнейших улучшений, можно было бы оформить этот код отдельной функцией и вызывать её для каждой требуемой лампы.

  1. Будем добавлять/править функции обработки нажатий клавиш:

    1. поправим функцию нажатия пробела (усложняется условие в if):

bool обработка_отжатия_клавиши_пробел(SDL_Event e) {
	if ((!прыжок) && (движение == 0) && (поворот == 0)) {
		прыжок = true;
		номер_кадра = 1;
	}
    return false;
}
    1. добавим 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;
}
    1. и добавим 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), но, как я уже писал вначале, тут мы всё делаем «по-простому».

  1. Нужно зарегистрировать все эти функции. Обработка клавиши Пробел у нас уже зарегистрирована в конце функции сцена(), для регистрации остальных добавьте там же эти строки:

    обработчик_событий.зарегистрировать_функцию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(&обработка_отжатия_клавиши_влево));
  1. Осталось доработать функцию обновления сцены. Добавьте эти два блока else if перед блоком else:

    1. обработка поворота:

	else if (поворот != 0) {
		место_персонажа.поворот(длительность_кадра * поворот * СКОРОСТЬ_ПОВОРОТА);
	}
    1. обработка движения:

	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, мы умножаем кватернион ориентации Места персонажа на этот вектор сдвига, и, таким образом, получаем вектор сдвига в нужную нам сторону.

  1. Нужно привязать камеру к объекту место_персонажа, чтобы она следовала за ним. Удалите или закомментируйте 2-ю строку функции сцена(), где мы в прошлом уроке задавали позицию камеры. Вместо неё добавьте следующие три строки перед блоком if:

	место_персонажа.добавить_ребёнка(менеджер.место_камеры);
	менеджер.место_камеры.задать_позицию(0,1.5,-3); // Камера "висит" в 3 метрах сзади на высоте полутора метров
	менеджер.место_камеры.задать_ориентацию(Кватернион(0,0,1,0)); // Камера повёрнута назад на 180 градусов
  1. Собственно, на этом всё. Компилируйте и запускайте программу. Если всё сделано правильно, то «Кленовый лист» должна ходить и поворачиваться при нажатии клавиш со стрелками, прыгать при нажатии пробела, а камера должна следовать за ней.


Конечно же, если выйти за границу пола, персонаж девушки никуда не упадёт, она так же продолжит ходить по (теперь уже воображаемой) плоскости. Чтобы персонаж «падал» с обрывов или «упирался» в стены, как это и должно быть в хорошей игре, к этому коду требуется ещё много чего добавить. Кроме того, всё что связано с мешем, скелетом и движениями персонажа, возможно, было бы правильнее оформить в отдельном модуле в виде класса.