[Перевод] Имитация 3D-персонажей в 2D-движке

Страницы:  1

Ответить
 

Professor Seleznov


pic
Необязательная предыстория
Я всегда был фанатом шутеров с видом сверху. Hotline Miami и по сей день остаётся моей любимой инди-игрой, а недавно я решил проиграть в её духовную наследницу — OTXO.
Он всегда был моим любимым жанром. Моя первая «серьёзная» игра, Alien Killer (написанная на Flash, поэтому сегодня в неё практически не поиграешь), была шутером с видом сверху, вдохновлённым старой игрой Net Yaroze

.
pic
Моя первая серьёзная игра на Flash: Alien Killer
Несколько лет спустя, когда я каждую пару месяцев выпускал новую игру на HTML5, мной был написан Warsim. На этот раз это была игра с процедурно генерируемыми картами, управлением только с клавиатуры и фиксацией на цели в стиле Tomb Raider.
В ней даже был многопользовательский режим, который помог мне понять, почему multiplayer так отличается от игры для одного.
pic
Warsim, не очень интересный однообразный шутер с видом сверху
Прошло больше десяти лет, но я всё равно почему-то возвращаюсь к этому жанру.
Чем-то он меня восхищает. Стрелять из дробовика в лица врагов, качая головой в ритм

— это моя атмосфера.
За прошедшие годы я пробовал и другие подходы, например, прототип игры с видом сверху, который рендерится только ASCII-артом:
Хоть мне по-прежнему нравится этот стиль, в итоге готовую игру я так и не сделал, но, возможно, однажды к этому вернусь.
Также я пробовал работать в настоящем 3D, но без особого успеха.
Больше всего мне нравится 2D, особенно когда можно использовать трюки для имитации перспективы. В этом есть собственный шарм без ограничений 3D.
(Мне пришлось поискать эти видео этих прототипов: 12345, 6)
[SWAGSHOT]
Изначально [SWAGSHOT] был очень простым проектом. Я хотел создать шутер с видом сверху с прицеливанием мышью и механикой замедления времени. В изначальной идее почти ничего другого не было, и, возможно, именно поэтому я с таким трудом пытался превратить прототип в игру.
pic
Но чего я хотел на самом деле, так это того, чтобы игра выглядела немного иначе. Мне хотелось попробовать немного другую перспективу камеры. По-прежнему в виде сверху и в 2D, но с большим упором на персонажа.
Копировать Hotline Miami смысла нет. Я не могу с ней конкурировать и не хочу создавать игру, которая уже существует.
Когда камера находится прямо над игроком, а всё создано в 2D, то ты довольно сильно ограничен в том, что можно передать графически.
Поэтому я задался вопросом: смогу ли я создать 2D-игру, которая рендерится процедурно (то есть без спрайтов) и выглядит достаточно трёхмерной?
После нескольких часов работы появился первый прототип:
Он довольно сильно отличается от того, как игра выглядит сегодня, но он убедил меня, что эта задача решаема.
Можно вручную анимировать несколько точек в пространстве, соединять их и создавать то, что выглядит, как персонаж-человек.
Кодинг
Создание 3D-скелета
Объясню, как я это делаю.
Первым делом я создаю 3D-скелет персонажа, которого хочу рендерить. Это выглядит так:
export class HumanoidSkeleton extends Skeleton {
readonly center = this.addPoint();
readonly leftFoot = this.addPoint();
readonly rightFoot = this.addPoint();
readonly leftKnee = this.addPoint();
readonly rightKnee = this.addPoint();
readonly leftHip = this.addPoint();
readonly rightHip = this.addPoint();
// ...
}
Когда у меня есть достаточно точек для приблизительного описания человека, мне нужно разместить их в пространстве.
Для этого нужно немного ручной работы, но это не так сложно, как можно было бы представить:
class TPoseAnimation extends ViewAnimation {
apply(skeleton: HumanoidSkeleton, age: number): void {
skeleton.center.x = 0;
skeleton.center.y = 0;
skeleton.center.z = 0;
skeleton.pelvis.x = 0;
skeleton.pelvis.y = 0;
skeleton.pelvis.z = 25;
skeleton.waist.x = 0;
skeleton.waist.y = 0;
skeleton.waist.z = 29;
// ...
}
}
Так мы описали одну анимацию, но их намного больше: анимация ходьбы, держания пистолета, рывка, переката…
Затем можно просто обновлять скелет следующим образом:
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"
}
}
Если отрендерить всё в таком виде, то это будет выглядеть так:
pic
Это явно гуманоид
Не особо интересно, правда?
Может, пусть он держит пистолет?
pic
Ну точно гуманоид
Всё ещё не очень похоже?
Трюк заключается в следующем: вместо того, чтобы брать 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;
}
}
Вот, как теперь выглядит «гуманоид».
pic
Уже ближе…
Если вы всё ещё не верите, то посмотрите, как это выглядит, когда я использую разные анимации и перемещаюсь:
pic
Облако точек в форме человека
Форма человека видна, но она выглядит, как движущаяся группа точек.
Давайте соединим эти точки жёлтыми линиями и увеличим сферу, обозначающую голову:
export class HumanoidView extends SkeletonRenderingView {
skeleton = new HumanoidSkeleton();
// Части
leftTibia = this.addPiece(new LineView(this.skeleton.leftKnee, this.skeleton.leftFoot, { color: 0xffff00, thickness: 2 }));
rightTibia = this.addPiece(new LineView(this.skeleton.rightKnee, this.skeleton.rightFoot, { color: 0xffff00, thickness: 2 }));
head = this.addPiece(new SphereView(this.skeleton.headCenter, { color: 0xffff00, radius: 2 }));
// ...
}
И вот результат:
pic
Теперь это уже походит на человека.
Избавимся от точек и добавим цвета:
pic
Теперь сделаем игру более пикселизованной, чтобы она соответствовала окружению и скрывала несовершенства:
pic
Анимации не обязаны быть идеальными, достаточно, чтобы они казались правильными.
Например, в некоторых случаях у персонажа будут странно изогнутые локти или слишком длинные ноги. Важно здесь то, что в такой перспективе это нас устраивает.
Некоторые анимации могут подходит для одной перспективы, но не для другой. Вот, что происходит, когда я увеличиваю значение перспективы:
pic
Голова становится слишком маленькой, а мушка пистолета — слишком длинной.
Чтобы добиться нужной картинки, приходится экспериментировать.
Тени
Теперь, когда у нас есть красиво анимированный персонаж, похожий на человека, добавим ему тень.
Трюк с тенями заключается в том, что мы рендерим точно такой же вид, но проекцию выполняем немного иначе.
Создадим функцию проецирования:
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);
}
}
Теперь для рендеринга тени можно создать другую проекцию:
export function projectShadowPoint(vec3: Vector3, vec2: Vector2) {
vec2.x = vec3.x - vec3.z * 0.5;
vec2.y = vec3.y + vec3.z * 0.5;
}
Добавим персонажу игрока второй 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]) {
// ... Применяем к скелетам анимации ...
}
}
При рендеринге это будет выглядеть так:
pic
Почти готово. Можно поменять цвета тени или применить цветовой фильтр:
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];
Результат:
pic
Также я реализовал несколько простых эффектов. Например, можно имитировать изменение источника освещения:
pic
Соединяем всё вместе
Я в общих чертах показал, как создать при помощи этой техники человека, но на самом деле эту систему можно использовать и для другого реквизита в игре: стеклянных панелей, столов, ящиков, растений…
В моей игре активно используются эти трюки. Вот, как это приблизительно выглядит, когда всё соединить вместе:
Планы на будущее
Надеюсь, этот пост будет вам полезен и, возможно, станет источником вдохновения.
По крайней мере, он позволил мне структурировать мысли.
Я попробую больше писать о том, чему учусь в процессе создания своих игр. Вот некоторые из тем, которые я собираюсь рассмотреть:
  • Создание кампании
  • Балансировка кампании при помощи метрик
  • Применение метрик для выполнения большинства игровых тестов
  • Создание ботов для использования в многопользовательских играх
  • Написание собственного игрового движка
  • Создание многопользовательской версии одной из моих игр
-Источник
 
Loading...
Error