Игра Морской бой на JavaScript. Расстановка кораблей методом перетаскивания.

Вступление.

Представляю вторую статью из цикла «Игра Морской бой на чистом JavaScript». В этой статье мы рассмотрим самостоятельную расстановку кораблей эскадры, путём перетаскивания их на игровое поле, используя метод Drag’n’Drop.
Прежде чем читать дальше, необходимо ознакомиться со статьёй «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.», иначе вам не всё будет понятно в этой статье.

Игра «Морской бой». Обработчик события подготовки к перетаскиванию кораблей.

Изначально корабли находятся… даже не знаю, как назвать место их первоначальной дислокации… Пусть это будет гавань или военно-морская база, кому как нравиться. Пока корабли не отображаются на экране. Их станет видно только после клика по псевдо-ссылке «Методом перетаскивания».

Запомните, это очень важно.
Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться элементы <span>, <button>, <div>, <li>. Именно на них вешаются обработчики событий.
Ни в коем случае, не используйте для этой цели тег <a>. Этот тег должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс.
Игра Морской бой. Пример игрового поля во время игры.

В предыдущей статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.» мы рассмотрели обработчик события начала расстановки кораблей, но в нём не было кода, запускающего расстановку кораблей перетаскиванием. Теперь рассмотрим этот код в полном объёме:

В дополненном коде появились две важные строки:

В первой строке мы создаём объект instance, используя конструктор Instance(), а во второй — вызываем функцию setObserver. Эта функция наследуется от объекта instance и устанавливает обработчики событий мыши.

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

Игра «Морской бой». Алгоритм перетаскивания кораблей шаг за шагом.

  • 1

    Регистрируем обработчики событий мышки, которые будут отслеживать эти события на двух элементах нашей HTML-вёрстки:
    1. контейнер с кораблями, которые нужно перетащить;
    2. игровое поле игрока, куда эти корабли будут перетаскиваться.

  • 2

    При нажатии левой кнопки мышки, проверяем, есть ли под курсором корабль. Готовим корабль к перемещению, для этого преобразуем его относительные координаты (относительно контейнера, в котором он находится) в абсолютные, относительно body. Для этого:
    — переместим div, обозначающий корабль, из родительского элемента в body;
    — визуально размещаем корабль на том же самом месте, используя уже не относительные, а абсолютные координаты относительно body.

  • 3

    Начинаем двигать курсор, отслеживая его координаты. Исходя из изменений координат курсора, меняем абсолютные координаты left/top корабля, что обеспечивает его визуальное перемещение.

  • 4

    При перетаскивании корабля постоянно проверяем валидность его координат:
    — если корабль находится в пределах игрового поля и рядом нет ранее установленных кораблей, то подсвечиваем контур корабля зелёным цветом;
    — во всех остальных случаях подсвечиваем контур красным цветом.

  • 5

    Отпускаем кнопку мыши. Если в данный момент корабль находится в пределах игрового поля и рядом нет ранее установленных кораблей, то:
    div, обозначающий корабль, переносим из корня body во внутрь div игрового поля;
    — присваиваем кораблю координаты, рассчитанные относительно игрового поля;
    — заносим данные корабля в соответствующие массивы.
    В противном случае корабль возвращается в то место, откуда его начали перетаскивать.

Игра «Морской бой». Перетаскивание корабля на игровое поле.

При использовании метода Drag’n’Drop мы опять будем использовать прототипное наследование, для этого создадим конструктор, который будет создавать клоны перетаскиваемых кораблей.

Как видите, конструктор сам очень простой и имеет только одно свойство — this.pressed, которое принимает два значения:
1. false — левая кнопка мышки не нажата;
2. true — левая кнопка нажата.
При этом, от этого конструктора наследуется большое количество функций, реализующих перетаскивание корабля.

Игра «Морской бой». Регистрация обработчиков событий мыши.

Регистрация обработчиков событий мыши реализована в функции setObserver. Прежде чем регистрировать обработчики, нужно создать два объекта, к которым эти обработчики будут привязаны:

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

Теперь можно прописать и сами обработчики событий, вызываемых мышкой:

Когда будем рассматривать редактирование положения кораблей перед началом игры в морской бой, мы добавим сюда ещё пару обработчиков, но пока ограничимся этими.
С помощью метода bind передаётся контекст в функцию, вызываемую обработчиком.

Полностью функция setObserver выглядит так:

Игра «Морской бой». Начало переноса корабля на игровое поле.

Инициирование переноса начинается с нажатия левой кнопки мыши на корабль, который находится в пределах контейнера initialShips, при этом запускается функция onMouseDown.

Алгоритм работы функции onMouseDown:

  • 1

    Проверяет, что нажатие было сделано на левую кнопку мыши, т. е. event.which == 1. Кроме этого, проверяется состояние флага startGame, информирующего о запуске игры.

  • 2

    Используя делегирование, определяет корабль, который подлежит перемещению. Если нажатие было сделано по пустой области initialShips, то работа функции прекращается.

  • 3

    Создаётся объект draggable, в котором запоминаются свойства переносимого элемента. Если перенос закончится вне игрового поля, то используя эти данные, можно вернуть корабль в исходную позицию с которой начался перенос.

  • Ниже представлен полный код функции:

    Игра «Морской бой». Визуальное перемещение корабля.

    Перемещение корабля происходит при срабатывании обработчика события mousemove, при этом корабль должен находиться под курсором, т. е. в зажатом состоянии. Данный обработчик обеспечивает не только визуальное перемещение корабля, но и постоянно проверяет его текущие координаты, сравнивая их с координатами игрового поля. Если перемещаемый корабль находится над игровым полем и в соседних клетках нет ранее поставленных кораблей, то он подсвечивается зелёным цветом. В противном случае контур корабля имеет красный цвет.

    Весь функционал перемещения корабля реализован в функции onMouseMove. Рассмотрим по шагам алгоритм работы данной функции:

    • 1

      Прежде чем функция начнёт работать, необходимо убедиться, что:
      1. нажата левая кнопка мыши, путём проверки состояние флага pressed;
      2. создан объект draggable, в котором содержится переносимый объект и его свойства.

    • 2

      Используя объект draggable, создаём клон корабля. Clone — это DOM-элемент, который мы и будем перемещать по экрану, задав ему position: absolute и изменяя свойства left и top.
      Запоминаем начальные координаты клона, чтобы в случае отмены переноса, вернуть его в исходное место на экране. Для этого будем использовать функцию rollback, которая является методом объекта clone.

      Перед тем, как начнём перетаскивать корабль, необходимо узнать, сколько у него палуб. Данная информация будет использоваться при расчете координат сторон клона. Исходя из этих координат мы можем определить, что перетаскиваемый объект полностью находится в границах игрового поля.

      Запомните, это очень важно.
      Клон корабля создаётся только в момент инициализации переноса и используется, пока вы не отпустите левую кнопку мыши.
    • 3

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

      Игра Морской бой. Смещение курсора.

      Как видно из рисунка, для того, чтобы сохранить положение курсора относительно левой-верхней точки клона, необходимо вычислить смещение курсора shiftX и shiftY и в дальнейшем использовать это смещение при формировании координат клона.

      При перемещении клона постоянно происходит валидация его координат. Первое, что проверяется — не находится ли перемещаемый корабль над игровым полем игрока. Если корабль переместился в координаты игрового поля, проверяется отсутствие в соседних координатах ранее установленных кораблей.
      При выполнении этих условий, контур клона окрашивается в зелёный цвет. Изменение цвета контура клона с красного на зелёный, информирует о том, что перемещение можно закончить, установив корабль в текущую позицию.

    Итак, с алгоритмом визуального перемещения корабля разобрались, теперь настало время начать писать JavaScript, реализующий этот алгоритм. Ранее говорилось, что перемещение корабля реализовано в функции onMouseMove. Прототипом данной функции является конструктор Instance. Вызов функции происходит при срабатывании обработчика события mousemove.

    Инициализация переноса начнётся, если выполнены два условия:

    1. нажата левая кнопка мыши;
    2. создан объект, в котором хранится информация о первоначальном положении корабля.

    Если условия выполнены, проверяем, существует ли объект clone и если нет, то создадим его.

    Подробный JS-код функции creatClone:

    Следует сделать уточнение относительно родительского элемента корабля. Если мы перетаскиваем корабль на игровое поле, то родительским элементом является контейнер, из которого мы корабли перетаскиваем. В случае редактирования положения корабля (редактирование будет рассмотрено ниже), родительским элементом является само игровое поле.

    Теперь необходимо вычислить смещение курсора от левого-верхнего угла клона:

    Для получения координат клона используется функция getCoords. Аргументом функции является элемент, координаты которого мы хотим получить. Координаты элемента вычисляются с помощью метода JavaScript — elem.getBoundingClientRect().
    Полный код функции getCoords:

    Для того, чтобы мы могли перетаскивать клон по всему экрану и при этом, клон находился поверх всех элементов HTML-страницы, необходимо:

    1. задать клону стили position: absolute; z-index: 1000;;
    2. сделать клон прямым потомком BODY для абсолютного позиционирования относительно документа.

    Напишем небольшой JS-код, который выполнит данные требования:

    Последнее, что нам необходимо сделать — это получить количество палуб корабля, клон которого мы собираемся перемещать. Для получения данной информации мы используем значение id, которое содержит в себе тип корабля. Теперь мы можем обратиться к массиву с данными кораблей shipsData и получить количество палуб, соответствующих данному типу.

    Для этого создадим функцию getCountDecks:

    Внимание.
    Более подробно о массиве shipsData изложено в описании конструктора Field в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»

    На этом инициирование перемещения закончено. Давайте посмотрим, как выглядит функция onMouseMove на данном этапе:

    Итак, клон перемещаемого корабля существует. Зададим ему новые координаты, которые будут соответствовать координатам курсора, с учётом его смещения относительно левого-верхнего угла клона.
    Теперь необходима валидация новых координат, т. е. нужно проверить, что клон полностью находится над игровым полем игрока. Для этого понадобятся координаты всех четырёх сторон клона. Координаты левой и верхней стороны уже получены. Это текущие координаты left/top. Чтобы получить координаты правой и нижней стороны, ещё раз запустим функцию getCoords.

    Добавим следующий код в конец функции onMouseMove:

    Для того, чтобы выяснить, находится клон над игровым полем или нет, достаточно сравнить координаты его сторон с координатами соответствующих сторон игрового поля. Координаты сторон клона мы только что получили, а координаты сторон игрового поля являются свойствами экземпляра user конструктора Field. Посмотрим, как будет выглядеть сравнение сторон:

    Согласитесь, позиционировать объект с точностью до пикселя достаточно затруднительно. Давайте разрешим небольшую погрешность позиционирования в размере 14px. В этом случае, если одна или две стороны клона будет выступать за пределы игрового поля не более чем на 14px, ошибкой считаться не будет. При отпускании кнопки мыши, эта погрешность будет устранена, а клон втянется в пределы игрового поля. Как это реализовано рассмотрим позже.
    Теперь условие сравнения сторон будет выглядеть следующим образом:

    Игра Морской бой. Перетаскивание корабля на игровое поле.

    Теперь проверим, что в соседних с клоном клетках нет ранее установленных кораблей. Для этого необходимо преобразовать абсолютные координаты клона в координаты матрицы связанные с сеткой игрового поля. Создадим новую функцию getCoordsClone, аргументом которой будет количество палуб перемещаемого клона. Прототипом функции getCoordsClone является конструктор Instance, поэтому её вызов будет выглядеть так:

    Рассмотрим по шагам алгоритм преобразования координат:

    • 1

      При помощи метода getBoundingClientRect получаем текущие абсолютные координаты каждой стороны клона.

    • 2

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

      Игра Морской бой. Преобразование координат клона корабля.
    • 3

      Преобразуем полученные координаты в пикселях в координаты матрицы, компенсируя при этом погрешности позиционирования, находящиеся в пределах +/-14px.

      Вспомним, что говорилось в предыдущей статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.» о взаимосвязи относительных координат в пикселях с координатами в сетке двумерной матрицы.
      Зная размер палубы корабля, он совпадает с размером клетки игрового поля и равен 33px, и зная смещение в пикселях относительно родительского элемента, можно преобразовать это смещение в координаты двумерной матрицы.

      Игра Морской бой. Преобразование координат.

    Полный код функции getCoordsClone:

    Зная координату первой палубы клона и количество его палуб, можно проверить, нет ли в соседних клетках ранее установленных кораблей. Для этого будем использовать функцию checkLocationShip.

    Внимание.
    Более подробно о функции checkLocationShip изложено в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»

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

    Теперь полный код проверки валидности координат будет выглядеть так:

    Добавим этот код код в конец функции onMouseMove.
    На этом визуальное перемещение корабля закончено.

    Игра «Морской бой». Окончание переноса корабля на игровое поле.

    Событие окончания переноса инициируется при отпускании левой кнопки мыши, запуская обработчик события onmouseup. Функционал обработки данного события реализован в функции onMouseUp, прототипом которой является конструктор Instance.

    Функция onMouseUp выполняет следующие ключевые задачи:

    • 1

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

    • 2

      Переносит div, обозначающий корабль, из корня BODY во внутрь div игрового поля.

    • 3

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

    • 4

      Заносит данные корабля в соответствующие массивы.

    • 5

      Очищает данные использованного клона и удаляет клон.

    Настало время написать JavaScript, реализующий данный алгоритм.
    Первое, что делает функция onMouseUp — проверяет существование объекта clone и, если данного объекта не существует (возможно был сделан клик вне переносимого элемента), то обработчик события onmouseup прекращает свою работу.

    Следующим шагом функция onMouseUp проверяет валидные или нет координаты клона в момент отпускания кнопки мыши. Нет необходимости проводить полную валидацию координат в том объёме, как это делалось в функции onMouseMove. Достаточно проверить класс, присвоенный клону:
    success — координаты валидны, можно продолжить работу обработчика событий;
    unsuccess — координаты не валидны.

    Если окажется, что корабль пытаются поставить в запретные координаты, то возвращаем его в место, откуда было начато его перемещение. Для этого будем использовать ранее созданную нами функцию rollback.
    После этого удаляем объекты clone и draggable, используя для этого функцию cleanClone. Данная функция очень простая и состоит всего из двух строк:

    Посмотрим, как теперь выглядит код функции onMouseUp на данном этапе:

    Теперь рассмотрим вариант, когда перемещаемый корабль устанавливается на игровое поле и рядом нет других кораблей, т. е. по валидным координатам.
    В этом случае, получаем координаты относительно игрового поля, используя ранее рассмотренную функцию getCoordsClone, затем переносим клон из корня BODY внутрь дива, формирующего игровое поле. После этого, полученные с помощью функции getCoordsClone координаты пропишем в атрибуте style.

    Описанный функционал реализуется с помощью следующего JS-кода:

    Итак, визуально корабль расположен там, где мы и хотели. Теперь необходимо, используя конструктор кораблей Ships, создать экземпляр данного корабля и сохранить информацию о нём в массивах matrix и squadron.

    Внимание.
    Более подробно о создании экземпляра корабля и конструкторе Ships написано в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»

    Теперь полный код функции onMouseUp, с учётом добавленного функционала, выглядит выглядит так:

    Итак, мы рассмотрели расстановку кораблей игрока с помощью метода Drag’n’Drop. В следующей статье будет рассказано, как с помощью JavaScript реализовать:
    — редактирование местоположения корабля;
    — изменение направления корабля путём поворота на 90°.

    Комментарии

    Всего: пока нет комментариев
    Требования при посте комментариев:
    1. Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
      Комментарии содержащие обсуждение политики, будут безжалостно удаляться.
    2. Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега <pre>:
      <pre class="lang:xhtml"> - HTML;
      <pre class="lang:css"> - CSS;
      <pre class="lang:javascript"> - JavaScript.
    3. Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *