Что нового
  • Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

Создаем Шутер От Первого Лица За 265 Строк Кода На Javascript

Sascha

Заместитель Администратора
Команда форума
Администратор
Регистрация
9 Май 2015
Сообщения
1,071
Баллы
155
Возраст
51
В этой статье мы создадим небольшой шутер от первого лица без сложной математики и техник 3D-визуализации, используя метод рейкастинга (трассировки, или «бросания», лучей).


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

— один из методов рендеринга в компьютерной графике, при котором сцена строится на основе замеров пересечения лучей с визуализируемой поверхностью.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Игрок


Логично, что если мы создаем шутер от первого лица, то наш игрок — это и есть точка, из которой будут выходить лучи. Для начала нам понадобится всего три свойства: координата x, координата y и направление:

function Player(x, y, direction) {
this.x = x;
this.y = y;
this.direction = direction;
}

Карта


Будем хранить карту с помощью двумерного массива. В нем 0 будет обозначать отсутствие стены, а 1 — её наличие. Для нашей реализации такой простой схемы будет достаточно.

function Map(size) {
this.size = size;
this.wallGrid = new Uint8Array(size * size);
}

Бросаем луч


Фишка в том, что при рейкастинге движок не рисует пространство целиком. Вместо этого он делит его на отдельные колонки и воспроизводит одну за одной. Каждая колонка представляет собой один брошенный под определенным углом луч. Если луч встречает на пути стену, он измеряет расстояние до нее и рисует прямоугольник в колонке. Высота прямоугольника определяется пройденным расстоянием — чем дальше стена, тем короче колонка.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Чем больше мы бросим лучей, тем более гладкими в результате будут переходы.

Найдем угол каждого луча


Угол зависит от трех параметров: направления, в котором смотрит игрок, фокусного расстояния камеры и колонки, которую мы в данный момент рисуем.

var x = column / this.resolution - 0.5;
var angle = Math.atan2(x, this.focalLength);
var ray = map.cast(player, player.direction + angle, this.range);

Проследим за каждым лучом на сетке


Нам нужно проверить наличие стен на пути каждого луча. В результате мы должны получить массив, в котором будут перечислены все стены, с которыми луч сталкивается, удаляясь от игрока.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Начинаем с того, что находим ближайшую к игроку горизонтальную stepX и вертикальную stepY линию сетки. Перемещаемся к той, что ближе, и проверяем на наличие стены с помощью inspect. Повторяем шаги до тех пор, пока не отследим до конца траекторию каждого луча.

function ray(origin) {
var stepX = step(sin, cos, origin.x, origin.y);
var stepY = step(cos, sin, origin.y, origin.x, true);
var nextStep = stepX.length2 < stepY.length2
? inspect(stepX, 1, 0, origin.distance, stepX.y)
: inspect(stepY, 0, 1, origin.distance, stepY.x);

if (nextStep.distance > range) return [origin];
return [origin].concat(ray(nextStep));
}

Обнаружить пересечения на сетке легко: нужно просто найти все целочисленные x (1, 2, 3…). А потом найти соответствующие y с помощью умножения x на коэффициент угла наклона rise / run.

var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);

Прелесть этой части алгоритма в том, что размер карты не имеет значения. Мы рассматриваем только определенный набор точек на сетке — каждый раз примерно одно и то же количество. В нашем примере размер карты 32×32, но если бы он был 32 000×32 000, скорость загрузки была бы такой же.

Рисуем колонку


После того как мы отследили луч, нам нужно нарисовать все стены, которые встречаются ему на пути.

var z = distance * Math.cos(angle);
var wallHeight = this.height * height / z;

Мы определяем высоту каждой стены, деля её максимальную высоту на z. Чем дальше стена, тем короче мы её рисуем.

Откуда взялся косинус? Если мы будем использовать «чистое» расстояние от игрока до стены, то в итоге получим эффект «рыбьего глаза». Представьте, что вы стоите лицом к стене. Края стены слева и справа находятся от вас дальше, чем центр стены. Но мы же не хотим, чтобы при отрисовке стена выпирала посередине? Для того, чтобы визуализировать плоскую стену так, как мы её видим в реальной жизни, мы строим треугольник из каждого луча и находим перпендикуляр к стене с помощью косинуса. Вот так:


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.


Слева — расстояние, справа — расстояние, умноженное на косинус угла.


В нашей статье это — самая сложная математика, с которой придется столкнуться

Визуализируем


Используем объект Camera, чтобы отрисовать карту с точки зрения игрока. Объект будет отвечать за визуализацию каждой колонки в процессе движения слева направо.

Прежде чем он отрисует стены, мы зададим skybox — большое изображение для фона с горизонтом и звездами. После того, как закончим со стенами, добавим оружие на передний план.

Camera.prototype.render = function(player, map) {
this.drawSky(player.direction, map.skybox, map.light);
this.drawColumns(player, map);
this.drawWeapon(player.weapon, player.paces);
};

Самые важные свойства камеры — разрешение, фокусное расстояние и диапазон.

  • Разрешение определяет, сколько колонок мы рисуем (сколько лучей бросаем);
  • Фокусное расстояние определяет ширину линзы, через которую мы смотрим (углы лучей);
  • Диапазон определяет дальность обзора (максимальная длина каждого луча).
Собираем воедино


Используем объект Controls для снятия данных с клавиш-стрелок и сенсорной панели, а также объект GameLoop для вызова requestAnimationFrame. Цикл игры прописываем всего тремя строками:

loop.start(function frame(seconds) {
map.update(seconds);
player.update(controls.states, map, seconds);
camera.render(player, map);
});

Детали

Дождь


Дождь симулируем с помощью нескольких очень коротких стен, разбросанных произвольно:

var rainDrops = Math.pow(Math.random(), 3) * s;
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);

ctx.fillStyle = '#ffffff';
ctx.globalAlpha = 0.15;
while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);

Задаем ширину стены в 1 пиксель.

Освещение и молнии


Освещение — это, вообще-то, работа с тенями: все стены рисуются со 100% яркостью, а потом покрываются черным прямоугольником какой-либо прозрачности. Прозрачность определяется как расстоянием до стены, так и её ориентацией (север / юг / запад / восток).

ctx.fillStyle = '#000000';
ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0);
ctx.fillRect(left, wall.top, width, wall.height);

Для симуляции молний, map.light случайным образом совершает резкий скачок до значения 2 и потом так же быстро гаснет.

Предупреждение столкновений


Для того, чтобы игрок не натыкался на стены, мы просто проверяем его следующую локацию по карте. Координаты x и y проверяем по отдельности, чтобы игрок мог идти вдоль стены:

Player.prototype.walk = function(distance, map) {
var dx = Math.cos(this.direction) * distance;
var dy = Math.sin(this.direction) * distance;
if (map.get(this.x + dx, this.y) <= 0) this.x += dx;
if (map.get(this.x, this.y + dy) <= 0) this.y += dy;
};

Текстура стен


Без текстуры стена выглядела бы довольно скучно. Для каждой колонки мы определяем текстуру посредством взятия остатка в точке пересечения луча со стеной.

step.offset = offset - Math.floor(offset);
var textureX = Math.floor(texture.width * step.offset);

Например, для пересечения в точке (10, 8,2) остаток равен 0,2. Это значит, что пересечение находится в 20% от левого края стены (8) и 80% от правого края (9). Поэтому мы умножаем 0,2 на texture.width чтобы найти x-координату для изображения текстуры.

Посмотреть результат можно

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

.
 
Вверх