Необязательная предыстория Я всегда был фанатом шутеров с видом сверху. Hotline Miami и по сей день остаётся моей любимой инди-игрой, а недавно я решил проиграть в её духовную наследницу — OTXO. Он всегда был моим любимым жанром. Моя первая «серьёзная» игра, Alien Killer (написанная на Flash, поэтому сегодня в неё практически не поиграешь), была шутером с видом сверху, вдохновлённым старой игрой Net Yaroze: .
Моя первая серьёзная игра на Flash: Alien Killer Несколько лет спустя, когда я каждую пару месяцев выпускал новую игру на HTML5, мной был написан Warsim. На этот раз это была игра с процедурно генерируемыми картами, управлением только с клавиатуры и фиксацией на цели в стиле Tomb Raider. В ней даже был многопользовательский режим, который помог мне понять, почему multiplayer так отличается от игры для одного.
Warsim, не очень интересный однообразный шутер с видом сверху Прошло больше десяти лет, но я всё равно почему-то возвращаюсь к этому жанру. Чем-то он меня восхищает. Стрелять из дробовика в лица врагов, качая головой в ритм — это моя атмосфера. За прошедшие годы я пробовал и другие подходы, например, прототип игры с видом сверху, который рендерится только ASCII-артом: Хоть мне по-прежнему нравится этот стиль, в итоге готовую игру я так и не сделал, но, возможно, однажды к этому вернусь. Также я пробовал работать в настоящем 3D, но без особого успеха. Больше всего мне нравится 2D, особенно когда можно использовать трюки для имитации перспективы. В этом есть собственный шарм без ограничений 3D. (Мне пришлось поискать эти видео этих прототипов: 1, 2, 3, 4, 5, 6) [SWAGSHOT] Изначально [SWAGSHOT] был очень простым проектом. Я хотел создать шутер с видом сверху с прицеливанием мышью и механикой замедления времени. В изначальной идее почти ничего другого не было, и, возможно, именно поэтому я с таким трудом пытался превратить прототип в игру. Но чего я хотел на самом деле, так это того, чтобы игра выглядела немного иначе. Мне хотелось попробовать немного другую перспективу камеры. По-прежнему в виде сверху и в 2D, но с большим упором на персонажа. Копировать Hotline Miami смысла нет. Я не могу с ней конкурировать и не хочу создавать игру, которая уже существует. Когда камера находится прямо над игроком, а всё создано в 2D, то ты довольно сильно ограничен в том, что можно передать графически. Поэтому я задался вопросом: смогу ли я создать 2D-игру, которая рендерится процедурно (то есть без спрайтов) и выглядит достаточно трёхмерной? После нескольких часов работы появился первый прототип: Он довольно сильно отличается от того, как игра выглядит сегодня, но он убедил меня, что эта задача решаема. Можно вручную анимировать несколько точек в пространстве, соединять их и создавать то, что выглядит, как персонаж-человек. Кодинг Создание 3D-скелета Объясню, как я это делаю. Первым делом я создаю 3D-скелет персонажа, которого хочу рендерить. Это выглядит так:
Когда у меня есть достаточно точек для приблизительного описания человека, мне нужно разместить их в пространстве. Для этого нужно немного ручной работы, но это не так сложно, как можно было бы представить:
Так мы описали одну анимацию, но их намного больше: анимация ходьбы, держания пистолета, рывка, переката… Затем можно просто обновлять скелет следующим образом:
const skeleton = new HumanoidSkeleton(); const animation = new TPoseAnimation(); animation.apply(skeleton, 0); // ... при необходимости применяем другие анимации ...
После этого все точки будут размещены в 3D-пространстве. Рендеринг скелета Но это просто точки, их нужно ещё отрендерить. Можно создать view, который рендерит эти точки:
export class HumanoidView extends SkeletonRenderingView { skeleton = new HumanoidSkeleton(); // Части leftKnee = this.addPiece(new SphereView(this.skeleton.leftKnee, { color: 0xff0000, radius: 2 })); rightKnee = this.addPiece(new SphereView(this.skeleton.rightKnee, { color: 0xff0000, radius: 2 })); // ... update() { for (const piece of this.pieces) { piece.update(); } } }
Здесь каждая точка скелета рендерится просто в виде «сферы». Сферы — это просто круглый спрайт, отрисовываемый в нужной точке. Выглядит это примерно так:
export class SphereView { sprite = new PIXI.Sprite(circleTexture); constructor(readonly pointIn3D: Vector3, params: { color: number, radius: number }) {} update() { sprite.position.x = this.pointIn3D.x; sprite.position.y = this.pointIn3D.y; // Обратите внимание, что пока мы игнорируем координату "z" } }
Если отрендерить всё в таком виде, то это будет выглядеть так:
Это явно гуманоид Не особо интересно, правда? Может, пусть он держит пистолет?
Ну точно гуманоид Всё ещё не очень похоже? Трюк заключается в следующем: вместо того, чтобы брать 3D-точку и рендерить её на плоскости, полностью игнорируя координаты Z, мы учитываем их при вычислении 2D-координат. Дополним наш класс SphereView:
const PERSPECTIVE = 1; // С этим значением можно поэкспериментировать и подобрать подходящее export class SphereView { sprite = new PIXI.Sprite(circleTexture); constructor(readonly pointIn3D: Vector3, params: { color: number, radius: number }) {} update() { this.sprite.position.x = this.pointIn3D.x; this.sprite.position.y = this.pointIn3D.y + this.pointIn3D.z * PERSPECTIVE; } }
Вот, как теперь выглядит «гуманоид».
Уже ближе… Если вы всё ещё не верите, то посмотрите, как это выглядит, когда я использую разные анимации и перемещаюсь:
Облако точек в форме человека Форма человека видна, но она выглядит, как движущаяся группа точек. Давайте соединим эти точки жёлтыми линиями и увеличим сферу, обозначающую голову:
И вот результат: Теперь это уже походит на человека. Избавимся от точек и добавим цвета: Теперь сделаем игру более пикселизованной, чтобы она соответствовала окружению и скрывала несовершенства: Анимации не обязаны быть идеальными, достаточно, чтобы они казались правильными. Например, в некоторых случаях у персонажа будут странно изогнутые локти или слишком длинные ноги. Важно здесь то, что в такой перспективе это нас устраивает. Некоторые анимации могут подходит для одной перспективы, но не для другой. Вот, что происходит, когда я увеличиваю значение перспективы: Голова становится слишком маленькой, а мушка пистолета — слишком длинной. Чтобы добиться нужной картинки, приходится экспериментировать. Тени Теперь, когда у нас есть красиво анимированный персонаж, похожий на человека, добавим ему тень. Трюк с тенями заключается в том, что мы рендерим точно такой же вид, но проекцию выполняем немного иначе. Создадим функцию проецирования:
const PERSPECTIVE = 1; export function projectWorldPoint(vec3: Vector3, vec2: Vector2) { vec2.x = vec3.x; vec2.y = vec3.y - vec3.z * PERSPECTIVE; } export class SphereView { sprite = new PIXI.Sprite(circleTexture); projection = projectWorldPoint; update() { // Как и раньше, но с преобразованием в projectWorldPoint this.projection(this.pointIn3D, this.sprite.position); } }
Теперь для рендеринга тени можно создать другую проекцию:
Добавим персонажу игрока второй HumanoidView, но дадим ему эту новую проекцию:
const playerView = new HumanoidView(); const shadowView = new HumanoidView(); for (const piece of shadowView.pieces) { piece.projection = projectShadowPoint; } function updatePlayerView() { for (const view of [playerView, shadowView]) { // ... Применяем к скелетам анимации ... } }
При рендеринге это будет выглядеть так: Почти готово. Можно поменять цвета тени или применить цветовой фильтр:
const filter = new PIXI.ColorMatrixFilter(); // Заменяем все цвета на чёрный filter.matrix[0] = 0; filter.matrix[6] = 0; filter.matrix[12] = 0; // Умножаем альфа-канал на 0.3 filter.matrix[18] = 0.3; shadowView.filters = [filter];
Результат: Также я реализовал несколько простых эффектов. Например, можно имитировать изменение источника освещения: Соединяем всё вместе Я в общих чертах показал, как создать при помощи этой техники человека, но на самом деле эту систему можно использовать и для другого реквизита в игре: стеклянных панелей, столов, ящиков, растений… В моей игре активно используются эти трюки. Вот, как это приблизительно выглядит, когда всё соединить вместе: Планы на будущее Надеюсь, этот пост будет вам полезен и, возможно, станет источником вдохновения. По крайней мере, он позволил мне структурировать мысли. Я попробую больше писать о том, чему учусь в процессе создания своих игр. Вот некоторые из тем, которые я собираюсь рассмотреть:
Создание кампании
Балансировка кампании при помощи метрик
Применение метрик для выполнения большинства игровых тестов
Создание ботов для использования в многопользовательских играх
Написание собственного игрового движка
Создание многопользовательской версии одной из моих игр