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

Вступление.

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

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

Набор кораблей, для перетаскивания на игровое поле, должен находиться в контейнере:

После загрузки игры «Морской бой» данный контейнер на экране не отображается, а если посмотреть HTML-код страницы, становится видно, что кроме информационной строки в нём ничего нет. При этом можно увидеть, что набор кораблей находится за пределами игрового поля, определяемого контейнером с классом battlefield в элементе <ul>.

Данный набор копируется в игровой контейнер и отображается на экране, только после нажатия на псевдоссылку «Методом перетаскивания». Это сделано для того, чтобы при выборе самостоятельной расстановки кораблей, всегда отображался их полный набор. Кроме этого, повторный клик на псевдоссылку, скрывает набор кораблей.

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

Прежде чем писать код для функции manually, рассмотрим алгоритм её работы:

  • 1

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

  • 2

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

  • 3

    Если набор кораблей был невидимым, мы должны его показать. Для этого:

    1. клонируем набор кораблей, находящийся в элементе <ul>;
    2. копируем клон в предназначенный для него контейнер shipsCollection
    3. задаём клону видимость, присваивая атрибуту hidden значение false.
  • 4

    В зависимости от результата вычисления в п. 1, показываем или скрываем контейнер shipsCollection

Кроме этого, независимо от выбранного способа расстановки кораблей, создаётся экземпляр класса Placement, отвечающего за перетаскивание кораблей на игровое поле и редактирование положения корабля на этом поле. Также вызывается метод этого класса — setObserver, устанавливающий обработчики событий мыши.

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

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

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

  • 1

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

  • 2

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

  • 3

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

  • 4

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

  • 5

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

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

Игра «Морской бой». Создание конструктора класса Placement.

Для реализации метода Drag’n’Drop мы будем использовать класс Placement.

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

FIELD
игровое поле игрока, именно на него мы будем перетаскивать корабли эскадры;
FRAME_COORDS
объект с координатами сторон игрового поля, по этим координатам будем определять, что перетаскиваемый корабль находится внутри игрового поля.

Конструктор имеет всего два свойства:

dragObject
объект, в который записываются все начальные данные перетаскиваемого корабля, эти данные понадобятся, если потребуется вернуть корабль в исходную точку;
pressed
флаг нажатия на левую кнопку мыши.

Кроме статических свойств, мы будем использовать пару статических функций:

getShipName
получает имя перетаскиваемого корабля, которое равно его ID;
getCloneDecks
получает количество палуб перетаскиваемого корабля.

Запишем полученный код:

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

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

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

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

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

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

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

  • 1

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

  • 2

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

  • 3

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

  • Напоминаю, что функция onMouseDown, как и все, ниже рассматриваемые функции, являются методами класса Placement, поэтому записывать их будем в области видимости этого класса.

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

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

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

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

    • 1

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

    • 2

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

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

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

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

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

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

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

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

    Как я писал выше, инициализация переноса начнётся, если выполнены два условия: нажата левая кнопка мыши и создан объект, в котором хранится информация о первоначальном положении корабля.
    Если условия выполнены, проверяем, существует ли объект clone и если нет, то создадим его.

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

    Рассмотрим подробно часть функции onMouseMove, отвечающую за создание клона:

    Как мы видим, при создании клона используются две функции: getCoordinates и creatClone. Функцию getCoordinates мы рассмотрели в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.», а вот функцию creatClone мы рассмотрим сейчас подробно.

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

    Итак, сначала функция creatClone сохраняет в свойстве clone перетаскиваемый элемент. Затем в свойстве oldPosition сохраняет сам объект dragObject, тем самым запоминаются начальные координаты перетаскиваемого корабля, его родительский элемент, а так же элемент следующий за ним.

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

    Также функция creatClone содержит метод объекта clonerollback. Данный метод позволяет вернуть перетаскиваемый корабль в исходную позицию, если при отпускании кнопки мыши, его координаты были невалидны. Координаты исходной позиции берутся из ранее созданного объекта oldPosition.

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

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

    Зададим созданному новые координаты, которые будут соответствовать координатам курсора, с учётом его смещения относительно левого-верхнего угла клона.

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

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

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

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

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

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

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

    • 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    • 1

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

    • 2

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

    • 3

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

    • 4

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

    • 5

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

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

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

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

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

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

    Рассмотрим новую функцию createShipAfterMoving, являющуюся методом класса Placement и создающую экземпляр нового корабля, на основе его клона. Задачи, решаемые данной функцией:

    1. Получает координаты клона, пересчитанные относительно игрового поля.
    2. Переносит клон из BODY внутрь игрового поля.
    3. Создаёт объект options со свойствами нового корабля.
    4. Используя объект options и класс Ships, создаёт экземпляр корабля.
    5. Удаляет клон из DOM.

    JS-код функции createShipAfterMoving:

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

    Итак, мы рассмотрели расстановку кораблей игрока с помощью метода 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 не будет опубликован.