HitBox — это чаще всего невидимая область или группа областей, которая помогает обнаруживать коснулся ли объект другого объекта, у которого тоже есть свой хитбокс.

В HTML страницах изначально нет понятие хитбокса у блоков, поэтому в этом посте мы сами получим и обработаем их с помощью JS.

Для чего нам нужны хитбоксы?

Чаще всего для каких-нибудь игрушек или же симуляция, где важно определить касания блоков.

1 пример: block1 коснулся block2 и произошло какое-нибудь действие.

2 пример: player стоял на block1, как только он отошел с него, сразу же упал

Почему мы не можем просто сравнить координаты?

Мы можем сравнить координаты X=left и Y=top, но наше условие будет выполняться только, если block1 идеально встанет на место block2, что будет выглядеть странно, да и если вы делается какую-нибудь игру/симуляцию, это будет неправильным решением

Универсальность и размер нашей функции

Наша функция JS будет работать со всеми блоками, если мы дадим доступ к ним.

Размер функции будет зависеть от кол-ва видов ваших блоков, но по стандарту она занимает не больше 30 строк кода

Все действия будут проходить через 2 цикла for():

1 цикл: Для определения углов блоков.

2 цикл: Определения касания блоков между собой.

Логика получения координат:

Допустим у нас есть 3 блока разного размера и разных цветов (красный, зеленый, синий) с начальными позициями:

У каждого блока есть координата X и Y, которые находятся в левом углу самого блока:

Но у блоков помимо координат, есть высота и ширина (height, width):

Прибавляя эти значения между собой, мы уже можем получить все нужные нам координаты: x1, x2, y1, y2.

Приступим к коду

Для начала в .html файле напишем базовый код и наш главный блок:

<body>
    <main class="mainBox">
  
    </main>
    
    <script src="script.js"></script> <!-- Заранее подключаем скрипт -->
</body>

Внутри mainBox засунем наши 3 блока и дадим им классы

<main class="mainBox">
    <div class="block red"></div>  <!-- Обращаться ко всем в JS будем через класс -->
    <div class="block green"></div>
    <div class="block blue"></div>
</main>

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

Сразу же даем класс и onclick:

<main class="mainBox">
    <div class="block red"></div>
    <div class="block green"></div>
    <div class="block blue"></div>
</main>
<button class="btn" onclick="getHitboxTouchResult()">Проверить</button>

Переходим в CSS и даем следующие стили:

body {
    margin: 0; /* Чтобы по краям не было отступов */
    background-color: rgb(60, 60, 60); /* Серый цвет приятнее глазу :D */
}
.mainBox {
    position: relative; /* Чтобы блоки могли использовать left, top в род. блоке */
}
.block {
    position: absolute; /* Возможность использования left, top */
}

/* Размеры и цвета доч. блоко */
.red {
    background-color: rgba(255, 0, 0, 0.6);
    width: 200px;
    height: 200px;
}
.blue {
    background-color: rgb(0, 0, 255, 0.6);
    width: 250px;
    height: 250px;
}
.green {
    background-color: rgba(0, 255, 0, 0.6);
    width: 100px;
    height: 100px;
}

.btn {
    position: absolute; /* Крепим кнопку снизу справа */
    right: 0;
    bottom: 0;
    width: 100px; /* Даем размер */
    height: 40px;
}

Получаем такой результат:

Все блоки слева сверху, давайте переместим их дав left, top параметры:

.red {
    background-color: rgba(255, 0, 0, 0.6);
    width: 200px;
    height: 200px;
    left: 920px;
    top: 320px;
}
.blue {
    background-color: rgb(0, 0, 255, 0.6);
    width: 250px;
    height: 250px;
    left: 690px;
    top: 190px;
}
.green {
    background-color: rgba(0, 255, 0, 0.6);
    width: 100px;
    height: 100px;
    left: 640px;
    top: 420px;
}

Теперь они у нас в таком положении по центру страницы:

Переходим в JS и обозначаем нашу главную функцию определения хитбоксов:

function setHitboxesAndCheck() {}

В нее записываем получения всех доч. блоков через класс block:

blocks = document.querySelectorAll(".block")

Все блоки у нас, теперь определим их координаты:

for (var i = 0; i < Object.keys(blocks).length; i++) { // повторяем цикл пока i не достигнет количества всех блоков
  
}

Определим координаты X:

for (var i = 0; i < Object.keys(blocks).length; i++) {
    // параметры offset могут записывать данные блока, в нашем случае мы используем offsetLeft и offsetWidth
    // за счет того, что blocks является массивом, мы можем каждому блоку присвоить переменные
    blocks[i].x1 = blocks[i].offsetLeft // первая точка X1
    blocks[i].x2 = blocks[i].offsetLeft + blocks[i].offsetWidth // вторая точка X2 за счет прибавления ширины 
}

Не забываем про Y:

for (var i = 0; i < Object.keys(blocks).length; i++) {
    blocks[i].x1 = blocks[i].offsetLeft
    blocks[i].x2 = blocks[i].offsetLeft + blocks[i].offsetWidth
    blocks[i].y1 = blocks[i].offsetTop
    blocks[i].y2 = blocks[i].offsetTop + blocks[i].offsetHeight // тут уже прибавляем высоту
}

Координаты наших 4 точек уже готовы, выглядят они так:

Проверяем касания наших точек

Для начала поймем как мы будем это делать.

Представим 2 блока, у которых есть свои точки:

Мы видим, что blocks[0].y2 находится в blocks[1].

Вначале будем проверять координаты X:

if (blocks[1].x1 <= blocks[0].x2) { // Если координата x1 второго блока меньше или равна координате x2 второго блока 
    if (blocks[0].x2 >= blocks[1].x1) { // Если координата x1 первого блока больше или равна координате x2 второго блока
        // В этих условия мы проверяем блоки именно касаются друг друга или просто находятся рядом
        console.log(true) // Если условия верны и блоки касаются по горизонтали, то мы выводим true
    }
}

Подключим циклы для проверок всех блоков:

for (var i = 0; i < Object.keys(blocks).length; i++) {
        for (var o = 0; o < Object.keys(blocks).length; o++) { // чтобы проверять блоки между собой
            if (i != o) { // даем условия, чтобы блоки не сравнивали свои же координаты
                if (blocks[i].x1 <= blocks[o].x2) { // наши условия
                    if (blocks[i].x2 >= blocks[o].x1) { // заменяем цифры на переменные
                        console.log(true)
                    }
                }
            }
        }
    }

Создаем еще одну функцию к которой подключим кнопку:

function getHitboxTouchResult() { // onclick нашей кнопки
    setHitboxesAndCheck()
}

Мы видим, что в консоли (F12) выводится true если они касаются по координате X.

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

Для этого дадим еще пару условий по координатам:

if (blocks[i].x1 <= blocks[o].x2) {
    if (blocks[i].x2 >= blocks[o].x1) {
      
        if (blocks[i].y1 <= blocks[o].y1) { // тут тоже самое, но в первом условии проверяем левые верхние углы между собой
            if (blocks[i].y2 >= blocks[o].y1) {
                console.log(true) // выводим true
            }
        }
      
    }
}

Теперь мы можем полностью определить хитбокс по горизонтали и вертикали.

Чуть изменим нашу функцию, чтобы мы могли использовать ее в сравнениях.

Заменим:

console.log(true)

На:

return true

Также заменим в функции кнопки, простой вызов на:

console.log(setHitboxesAndCheck())

Для использования нескольких видов блоков, нужно будет лишь их указывать в сравнениях.

Весь код:

HTML

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hitboxes</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <main class="mainBox">
        <div class="block red"></div>
        <div class="block green"></div>
        <div class="block blue"></div>
    </main>
    <button class="btn" onclick="getHitboxTouchResult()">Check</button>

    <script src="script.js"></script>
</body>

</html>

CSS

body {
    margin: 0;
    background-color: rgb(60, 60, 60);
}
.mainBox {
    position: relative;
}
.block {
    position: absolute;
}
.red {
    background-color: rgba(255, 0, 0, 0.6);
    width: 200px;
    height: 200px;
    left: 920px;
    top: 320px;
}
.blue {
    background-color: rgb(0, 0, 255, 0.6);
    width: 250px;
    height: 250px;
    left: 690px;
    top: 190px;
}
.green {
    background-color: rgba(0, 255, 0, 0.6);
    width: 100px;
    height: 100px;
    left: 640px;
    top: 420px;
    display: none;
}
.btn {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 100px;
    height: 40px;
}

JS

function setHitboxesAndCheck() {
    blocks = document.querySelectorAll(".block")
    for (var i = 0; i < Object.keys(blocks).length; i++) {
        blocks[i].x1 = blocks[i].offsetLeft
        blocks[i].x2 = blocks[i].offsetLeft + blocks[i].offsetWidth
        blocks[i].y1 = blocks[i].offsetTop
        blocks[i].y2 = blocks[i].offsetTop + blocks[i].offsetHeight
    }
    for (var i = 0; i < Object.keys(blocks).length; i++) {
        for (var o = 0; o < Object.keys(blocks).length; o++) {
            if (i != o) {
                if (blocks[i].x1 <= blocks[o].x2) {
                    if (blocks[i].x2 >= blocks[o].x1) {
                        if(blocks[i].y1 <= blocks[o].y1) {
                            if(blocks[i].y2 >= blocks[o].y1) {
                                return true
                            }
                        }
                    }
                }
            }
        }
    }
}
function getHitboxTouchResult() {
    console.log(setHitboxesAndCheck())
}

Спасибо за прочтение данного поста.

Буду благодарен, если посетите мой телеграм канал: https://t.me/blg_projects

Комментарии (9)


  1. wdhappyk
    27.09.2022 22:19
    +3

    Данный код не будет работать корректно, если элементы будут находится в разных контейнерах с position != static, т.к. координаты будут рассчитываться уже относительно родителя, а не документа.
    https://stackblitz.com/edit/angular-ivy-hbzpmj?file=src%2Fapp%2Fapp.component.html,src%2Fapp%2Fapp.component.css,src%2Fstyles.css
    Посмотрите на данный пример. Изменение у .item position со static на relative изменяет offset-ы дочернего элемента.


  1. k12th
    27.09.2022 14:27

    А чего не через getBoundingClientRect?


    1. Samogon4ik Автор
      27.09.2022 14:48
      -2

      Можно и через него.Тут уже сам выбираешь, через что получать значения


  1. didyk_ivan
    27.09.2022 14:49

    А зачем всё это делать ?

    (конечно, если не считать игры).


    1. Samogon4ik Автор
      27.09.2022 14:49

      Симуляции, анимации с физикой


  1. Metotron0
    27.09.2022 14:50

    Ожидал, что будет учтён transform: rotate, потому что без него тут пробле не видно. Понятно, что и с поворотами давно уже всё решено, но хотя бы на школьном уровне решение можно было представить.


  1. wdhappyk
    27.09.2022 22:19
    +3

    Данный код не будет работать корректно, если элементы будут находится в разных контейнерах с position != static, т.к. координаты будут рассчитываться уже относительно родителя, а не документа.
    https://stackblitz.com/edit/angular-ivy-hbzpmj?file=src%2Fapp%2Fapp.component.html,src%2Fapp%2Fapp.component.css,src%2Fstyles.css
    Посмотрите на данный пример. Изменение у .item position со static на relative изменяет offset-ы дочернего элемента.


    1. Samogon4ik Автор
      27.09.2022 22:22
      +2

      К сожалению я это не учел.Как я понимаю стоило использовать getBoundingClientRect

      Закреплю дабы люди знали о подобной проблеме


  1. bazrik
    27.09.2022 22:20
    +1

    А через Intersection Observer разве нельзя это сделать?


    1. Metotron0
      28.09.2022 05:32

      По-моему, там есть какое-то условие по вложенности пересекающихся элементов. То ли у них общий родитель, то ли что-то похожее. Пишут, что элемент, за которым следим, должен лежать в root, означает ли это что можно отслеживать только пересечение элементов, один из которых потомок другого?