Stage 6: Прочее
Предметы
Данная страница еще в разработке, уверены, что хотите ознакомиться?
2.5D Renderer
2D Renderer
Управление камерой WASD и ZX
На этом шаге добавим к сектору простые предметы. Пока это не полноценные спрайты с прозрачностью и текстурой, а отладочные прямоугольные области: они показывают, где на 2.5D-экране будет рисоваться предмет. Важно, что предмет принадлежит конкретному сектору, поэтому его вертикальные границы рассчитываются с учетом высоты пола и потолка этого сектора.
В описание сектора добавляем массив items. Каждый Item хранит позицию в плоскости карты, необязательное смещение по высоте, радиус и тип. Поле spriteоставлено для следующего шага: позже вместо одноцветного прямоугольника можно будет читать пиксели из bitmap.
interface Sector {
// ..
items?: Item[];
}
interface Item {
type: string;
x: number;
y: number;
z?: number;
radius: number;
sprite?: {
width: number;
height: number;
colors: Color[];
bitmap: number[][];
};
}Предметы задаются внутри того сектора, на полу которого они стоят. Например, аптечка лежит во втором секторе:
const stepSector2: Sector = {
// ..
floorHeight: 750,
ceilHeight: 10_000,
items: [
{
x: startX + 50 * 1.5,
y: startY + 25,
radius: 10,
type: 'health'
}
]
};Во время отрисовки кадра рендерер обходит BSP-дерево и встречает сегменты разных секторов. Для каждого сектора, у которого есть items, мы один раз вычисляем экранные границы всех его предметов. Сектор запоминается в наборе уже обработанных, чтобы один и тот же предмет не попал в список несколько раз через разные стены этого сектора.
Сначала предмет переводится в координаты относительно камеры. Скалярное произведение с направлением взгляда дает расстояние вперед, а угол до предмета превращается в координату screenX. Если предмет находится за камерой или его угловая область не пересекает FOV, он не рисуется.
const dx = item.x - camera.x;
const dy = item.y - camera.y;
const forwardDistance = dx * camera.angle.cos + dy * camera.angle.sin;
const distance = Math.hypot(dx, dy);
const angleToItem = Math.atan2(dy, dx) * 180 / Math.PI;
const relativeAngle = angleToItem - camera.angle.degrees;
const screenX =
(relativeAngle / camera.fov.degrees) * camera.screen.width
+ camera.screen.width / 2;Горизонтальная ширина берется из углового радиуса предмета. Чем ближе предмет, тем большеMath.atan2(radius, distance), а значит шире его область на экране. Вертикальные границы считаются отдельно: низ прямоугольника привязан к полу сектора плюсitem.z, верх — к потолку сектора.
const itemAngularRadius = Math.atan2(item.radius, distance) * 180 / Math.PI;
const screenHalfWidth =
(itemAngularRadius / camera.fov.degrees) * camera.screen.width;
const projectionScale = 1 / Math.max(forwardDistance, 1);
const floorZ = (sector.floorHeight ?? 0) + (item.z ?? 0);
const ceilZ = sector.ceilHeight ?? floorZ;
const screenBottomY = screenCenterY - (floorZ - camera.z) * projectionScale;
const screenTopY = screenCenterY - (ceilZ - camera.z) * projectionScale;В итоге у предмета получается прямоугольник в экранных координатах:x1,x2,y1 иy2. Эти границы обрезаются по размеру canvas, а пустые прямоугольники отбрасываются.
const x1 = Math.max(0, Math.floor(screenX - screenHalfWidth));
const x2 = Math.min(camera.screen.width, Math.ceil(screenX + screenHalfWidth));
const y1 = Math.max(0, Math.floor(screenTopY));
const y2 = Math.min(camera.screen.height, Math.ceil(screenBottomY));После обхода BSP все найденные предметные области сортируются по расстоянию от дальних к ближним и рисуются поверх геометрии. Сейчас цвет выбирается по type предмета и затемняется яркостью сектора. На следующем шаге этот прямоугольник станет рамкой, внутри которой можно будет выбирать пиксели настоящего спрайта.