Stage 4: Улучшаем движение
Коллизии со стенами
Данная страница еще в разработке, уверены, что хотите ознакомиться?
2.5D Renderer
2D Renderer
В прошлом шаге камера уже учитывала высоту сектора, но всё ещё могла проходить сквозь стены. Для движения это выглядит странно: рендерер честно рисует стену, а позиция камеры при этом оказывается по другую сторону от нее. В этом шаге добавим простую 2D-коллизию.
Игрока будем считать не точкой, а кругом с небольшим радиусом. Так камера не сможет подойти вплотную к стене и пересечь ее центром. Проверяем только твердые стены: односторонние стены и закрытые seg блокируют движение, а проходы между секторами остаются проходимыми.
export const DEFAULT_CONFIG = {
playerRadius: 5,
stepHeight: 1000
};
export function isSolidWall(seg: Seg): boolean {
return Boolean(!seg.isTwoSide || seg.isSolid);
}
Чтобы понять, пересекся ли круг игрока со стеной, сначала ищем ближайшую точку на отрезке стены. Проекция точки
на отрезок дает параметр t,
а затем мы ограничиваем его диапазоном 0..1,
чтобы ближайшая точка не ушла за концы стены.
export function closestPointOnSegment(seg: Seg, point: Vertex): Vertex {
const ax = seg.end.x - seg.start.x;
const ay = seg.end.y - seg.start.y;
const len2 = ax * ax + ay * ay;
let t = ((point.x - seg.start.x) * ax + (point.y - seg.start.y) * ay) / len2;
t = Math.max(0, Math.min(1, t));
return {
x: seg.start.x + ax * t,
y: seg.start.y + ay * t
};
}
Дальше сравниваем расстояние от центра игрока до этой ближайшей точки с радиусом игрока. Если расстояние меньше
радиуса, круг заехал внутрь стены. В этом случае считаем глубину пересечения
overlap и получаем вектор,
который вытолкнет камеру обратно наружу.
export function circleSegmentCollision(center: Vertex, radius: number, seg: Seg) {
const closest = closestPointOnSegment(seg, center);
const dx = center.x - closest.x;
const dy = center.y - closest.y;
const distance = Math.hypot(dx, dy);
if (distance >= radius) {
return { collided: false, pushX: 0, pushY: 0, distance };
}
const overlap = radius - distance;
return {
collided: true,
pushX: nx * overlap,
pushY: ny * overlap,
distance
};
}
В движении мы больше не записываем желаемые координаты напрямую. Сначала считаем, куда камера хочет попасть, затем
передаем старую и новую позицию в checkCollisionOptimized.
Функция возвращает уже исправленную позицию: если столкновения нет, это почти те же координаты; если есть, камера
будет слегка сдвинута от стены.
const collision = checkCollisionOptimized(
camera.x,
camera.y,
camera.x + moveX,
camera.y + moveY,
DEFAULT_CONFIG.playerRadius,
settings.level.linedefs,
true
);
return {
...camera,
x: collision.x,
y: collision.y,
z: newZ,
};
Здесь же появляется проверка высоты ступеньки. Портал между секторами может быть проходом, но пол за ним может
оказаться слишком высоким. Поэтому сравниваем старую высоту пола с высотой пола нового сектора. Если подъем меньше
допустимого stepHeight, камера
поднимается на ступеньку. Если выше - движение отменяется.
const oldFloorZ = camera.z! - height;
const newFloorZ = sector.floorHeight!;
const stepUp = newFloorZ - oldFloorZ;
if (stepUp > 0 && stepUp <= DEFAULT_CONFIG.stepHeight) {
newZ = sector.floorHeight! + height;
} else if (stepUp > DEFAULT_CONFIG.stepHeight) {
return { ...camera };
}
Оптимизированная проверка не перебирает все стены уровня вслепую. Сначала строится небольшой прямоугольник вокруг перемещения камеры с учетом радиуса игрока, и в подробную проверку попадают только seg, чьи bounding box пересекают этот прямоугольник. Для учебного уровня это не критично, но такая привычка быстро окупается на больших картах.