Игра Морской бой на JavaScript. Расстановка кораблей методом перетаскивания.
Вступление.
Представляю вторую статью из цикла «Игра Морской бой на чистом JavaScript». В этой статье мы рассмотрим самостоятельную расстановку кораблей эскадры, путём перетаскивания их на игровое поле, используя метод Drag’n’Drop.
Прежде чем читать дальше, необходимо ознакомиться со статьёй «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.», иначе вам не всё будет понятно в этой статье.
Игра «Морской бой». Обработчик события подготовки к перетаскиванию кораблей.
Набор кораблей, для перетаскивания на игровое поле, должен находиться в контейнере:
1 2 3 |
<div id="ships_collection" class="ships-collection" hidden></div> |
После загрузки игры «Морской бой» данный контейнер на экране не отображается, а если посмотреть HTML-код страницы, становится видно, что кроме информационной строки в нём ничего нет. При этом можно увидеть, что набор кораблей находится за пределами игрового поля, определяемого контейнером с классом battlefield
в элементе <ul>
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<ul class="initial-ships" hidden> <li> <div id="fourdeck1" class="ship fourdeck"></div> <div id="tripledeck1" class="ship tripledeck tripledeck1"></div> <div id="tripledeck2" class="ship tripledeck tripledeck2"></div> </li> <li> <div id="doubledeck1" class="ship doubledeck"></div> <div id="doubledeck2" class="ship doubledeck doubledeck2"></div> <div id="doubledeck3" class="ship doubledeck doubledeck3"></div> </li> <li> <div id="singledeck1" class="ship singledeck"></div> <div id="singledeck2" class="ship singledeck singledeck2"></div> <div id="singledeck3" class="ship singledeck singledeck3"></div> <div id="singledeck4" class="ship singledeck singledeck4"></div> </li> </ul> |
Данный набор копируется в игровой контейнер и отображается на экране, только после нажатия на псевдоссылку «Методом перетаскивания». Это сделано для того, чтобы при выборе самостоятельной расстановки кораблей, всегда отображался их полный набор. Кроме этого, повторный клик на псевдоссылку, скрывает набор кораблей.
Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться элементы
<span>
, <button>
, <div>
, <li>
. Именно на них вешаются обработчики событий.Не используйте для этой цели тег
<a>
. Этот тег должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс.
Прежде чем писать код для функции manually
, рассмотрим алгоритм её работы:
-
1
Определяем видимость блока с набором кораблей. Это необходимо для того, чтобы понять, мы будем скрывать или показывать данный блок.
-
2
Проверяем наличие в контейнере других элементов (набора кораблей), кроме информационной строки. Если есть, удаляем. Нам не нужен старый набор, т. к. часть кораблей в нём может уже отсутствовать — их перетащили на игровое поле.
-
3
Если набор кораблей был невидимым, мы должны его показать. Для этого:
- клонируем набор кораблей, находящийся в элементе
<ul>
; - копируем клон в предназначенный для него контейнер
shipsCollection
- задаём клону видимость, присваивая атрибуту
hidden
значениеfalse
.
- клонируем набор кораблей, находящийся в элементе
-
4
В зависимости от результата вычисления в п. 1, показываем или скрываем контейнер
shipsCollection
Кроме этого, независимо от выбранного способа расстановки кораблей, создаётся экземпляр класса Placement
, отвечающего за перетаскивание кораблей на игровое поле и редактирование положения корабля на этом поле. Также вызывается метод этого класса — setObserver
, устанавливающий обработчики событий мыши.
В предыдущей статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.» мы рассмотрели обработчик события начала расстановки кораблей, но в нём не было кода, запускающего расстановку кораблей перетаскиванием. Дополним этот код :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
getElement('type_placement').addEventListener('click', function(e) { // используем делегирование основанное на всплытии событий if (e.target.tagName != 'SPAN') return; // если мы уже создали эскадру ранее, то видна кнопка начала игры // скроем её на время повторной расстановки кораблей buttonPlay.dataset.hidden = true; // очищаем игровое поле игрока перед повторной расстановкой кораблей human.cleanField(); // очищаем клон объекта с набором кораблей let initialShipsClone = ''; // способ расстановки кораблей на игровом поле const type = e.target.dataset.target; // создаём литеральный объект typeGeneration // каждому свойству литерального объекта соответствует функция // в которой вызывается рандомная или ручная расстановка кораблей const typeGeneration = { random() { // скрываем контейнер с кораблями, предназначенными для перетаскивания // на игровое поле shipsCollection.hidden = true; // вызов ф-ии рандомно расставляющей корабли для экземпляра игрока human.randomLocationShips(); }, manually() { // определяем видимость набора кораблей let value = !shipsCollection.hidden; // если в контейнере, кроме информационной строки, находится набор // кораблей, то удаляем его if (shipsCollection.children.length > 1) { shipsCollection.removeChild(shipsCollection.lastChild); } // если набор кораблей при клике на псевдоссылку был невидим, то // клнируем его, переносим в игровой контейнер и выводим на экран if (!value) { initialShipsClone = initialShips.cloneNode(true); shipsCollection.appendChild(initialShipsClone); initialShipsClone.hidden = false; } // в зависимости от полученного значения value показываем или скрываем // блок с набором кораблей shipsCollection.hidden = value; } }; // вызов функции литерального объекта в зависимости // от способа расстановки кораблей typeGeneration[type](); // создаём экземпляр класса, отвечающего за перетаскивание // и редактирование положения кораблей const placement = new Placement(); // устанавливаем обработчики событий placement.setObserver(); }); |
Прежде чем приступать к подробному рассмотрению функций, обеспечивающих перетаскивание кораблей на игровое поле, подробно рассмотрим алгоритм, которого мы будем придерживаться, разрабатывая данный функционал.
Игра «Морской бой». Алгоритм перетаскивания кораблей шаг за шагом.
-
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
- получает количество палуб перетаскиваемого корабля.
Запишем полученный код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Placement { // объект с координатами стророн игрового поля static FRAME_COORDS = getCoordinates(humanfield); constructor() { // объект перетаскивамого корабля this.dragObject = {}; // флаг нажатия на левую кнопку мыши this.pressed = false; } // имя перетаскиваемого корабля static getShipName = el => el.getAttribute('id'); // количество палуб у перетаскиваемого корабля static getCloneDecks = el => { // чтобы получить тип корабля, уберём из его имени последний // символ, являющийся цифрой const type = Placement.getShipName(el).slice(0, -1); return Field.SHIP_DATA[type][1]; } } |
Игра «Морской бой». Регистрация обработчиков событий мыши.
Регистрация обработчиков событий мыши реализована в функции setObserver
.
Обработчики событий вешаются не на сами корабли, а на их родительские элементы. Таким образом мы значительно сократим количество используемых обработчиков. Для того, чтобы определить, какой именно корабль будет перемещаться, используется делегирование. Подробно об этом будет рассказано ниже.
Полностью функция setObserver
выглядит так:
1 2 3 4 5 6 7 8 9 10 11 |
setObserver() { // если обработчики уже установлены, выходим из функции if (isHandlerPlacement) return; document.addEventListener('mousedown', this.onMouseDown.bind(this)); document.addEventListener('mousemove', this.onMouseMove.bind(this)); document.addEventListener('mouseup', this.onMouseUp.bind(this)); // флаг установки обработчиков isHandlerPlacement = true; } |
Флаг isHandlerPlacement
необходим для того, чтобы исключить многократную установку обработчиков, при неоднократном нажатии на псевдоссылки выбора способа размещения кораблей.
С помощью метода bind
передаётся контекст в функцию, вызываемую обработчиком.
Игра «Морской бой». Начало переноса корабля на игровое поле.
Инициирование переноса начинается с нажатия левой кнопки мыши на корабль, который находится в пределах контейнера initialShips
, скопированного в контейнер shipsCollection
. При этом запускается функция onMouseDown
.
1 2 3 4 5 6 7 8 9 10 11 |
<div id="ships_collection" class="ships-collection"> <p>Перетащите мышкой корабли на игровое поле. Для установки корабля по вертикали, кликните по нему правой кнопкой мышки.</p> <ul class="initial-ships"> <!-- Здесь расположены корабли, предназначенные для перетаскивания на игровое поле игрока --> </ul> </div> |
Алгоритм работы функции onMouseDown
:
-
1
Проверяет, что нажатие было сделано на левую кнопку мыши, т. е.
event.which == 1
. Кроме этого, проверяется состояние флагаstartGame
, информирующего о запуске игры. -
2
Используя делегирование, определяет корабль, который подлежит перемещению. Если нажатие было сделано по пустой области контейнера
initialShips
, то работа функции прекращается. -
3
Создаётся объект
dragObject
, в котором запоминаются свойства переносимого элемента. Если перенос закончится вне игрового поля, то используя эти данные, можно вернуть корабль в исходную позицию с которой начался перенос. -
1
Прежде чем функция начнёт работать, необходимо убедиться, что:
1. нажата левая кнопка мыши, путём проверки состояние флагаpressed
;
2. создан объектdragObject
, в котором содержится переносимый объект и его свойства.123if (this.pressed == false || !this.dragObject.elem) return; -
2
Используя объект
dragObject
, создаём клон корабля.Clone
— это DOM-элемент, который мы и будем перемещать по экрану, задав емуposition: absolute
и изменяя свойстваleft
иtop
.
Запоминаем начальные координаты клона, чтобы в случае отмены переноса, вернуть его в исходное место на экране. Для этого будем использовать функциюrollback
, которая является методом объектаclone
.Перед тем, как начнём перетаскивать корабль, необходимо узнать, сколько у него палуб. Данная информация будет использоваться при расчете координат сторон клона. Исходя из этих координат мы можем определить, что перетаскиваемый объект полностью находится в границах игрового поля.
Запомните, это очень важно.
Клон корабля создаётся только в момент инициализации переноса и используется, пока вы не отпустите левую кнопку мыши. -
3
Непосредственно само перетаскивание корабля, а точнее его клона, происходит при перемещении курсора мыши. При этом клону присваиваются координаты курсора. Вроде бы всё просто и логично, но за этой простотой кроется очень неприятный визуальный эффект. Посмотрим внимательно на рисунок:
Как видно из рисунка, для того, чтобы сохранить положение курсора относительно левой-верхней точки клона, необходимо вычислить смещение курсора
shiftX
иshiftY
и в дальнейшем использовать это смещение при формировании координат клона.При перемещении клона постоянно происходит валидация его координат. Первое, что проверяется — не находится ли перемещаемый корабль над игровым полем игрока. Если корабль переместился в координаты игрового поля, проверяется отсутствие в соседних координатах ранее установленных кораблей.
При выполнении этих условий, контур клона окрашивается в зелёный цвет. Изменение цвета контура клона с красного на зелёный, информирует о том, что перемещение можно закончить, установив корабль в текущую позицию. -
1
Получаем разность между координатой стороны клона и координатой соответствующей стороны игрового поля. Таким образом, мы получаем значение координат клона относительно игрового поля.
-
2
Преобразуем полученные координаты в пикселях в координаты матрицы, компенсируя при этом погрешности позиционирования, находящиеся в пределах +/-14px.
Вспомним, что говорилось в предыдущей статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.» о взаимосвязи относительных координат в пикселях с координатами в сетке двумерной матрицы.
Зная размер палубы корабля, он совпадает с размером клетки игрового поля и равен 33px, и зная смещение в пикселях относительно родительского элемента, можно преобразовать это смещение в координаты двумерной матрицы. -
1
Возвращает корабль в исходную позицию, если координаты клона, в момент отпускания кнопки мыши, были невалидны.
-
2
Переносит элемент, обозначающий корабль, из корня
BODY
вовнутрь контейнера игрового поля. -
3
Присваивает кораблю координаты, рассчитанные относительно игрового поля.
-
4
Заносит данные корабля в соответствующие массивы.
-
5
Очищает данные использованного клона и удаляет клон.
- Получает координаты клона, пересчитанные относительно игрового поля.
- Переносит клон из BODY внутрь игрового поля.
- Создаёт объект
options
со свойствами нового корабля. - Используя объект
options
и классShips
, создаёт экземпляр корабля. - Удаляет клон из
DOM
. -
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
Напоминаю, что функция onMouseDown
, как и все, ниже рассматриваемые функции, являются методами класса Placement
, поэтому записывать их будем в области видимости этого класса.
Ниже представлен полный код функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
onMouseDown(e) { // если нажата не левая кнопка мыши или игра уже запущена if (e.which != 1 || startGame) return; // проверяем, что нажатие произошло над кораблём const el = e.target.closest('.ship'); if(!el) return; this.pressed = true; // переносимый объект и его свойства this.dragObject = { el, parent: el.parentElement, next: el.nextElementSibling, // координаты, с которых начат перенос downX: e.pageX, downY: e.pageY, // координаты 'left' и 'top' используются при редактировании // положения корабля на игровом поле left: el.offsetLeft, top: el.offsetTop, // горизонтальное положение корабля kx: 0, ky: 1 }; } |
Игра «Морской бой». Алгоритм визуального перемещение корабля.
Перемещение корабля происходит при срабатывании обработчика события mousemove
, при этом корабль должен находиться под курсором, т. е. в зажатом состоянии. Данный обработчик обеспечивает не только визуальное перемещение корабля, но и постоянно проверяет его текущие координаты, сравнивая их с координатами игрового поля. Если перемещаемый корабль находится над игровым полем и в соседних клетках нет ранее поставленных кораблей, то он подсвечивается зелёным цветом. В противном случае контур корабля имеет красный цвет.
Весь функционал перемещения корабля реализован в функции onMouseMove
. Рассмотрим по шагам алгоритм работы данной функции:
Итак, с алгоритмом визуального перемещения корабля разобрались, теперь настало время начать писать JavaScript, реализующий этот алгоритм. Ранее говорилось, что перемещение корабля реализовано в функции onMouseMove
, являющейся методом класса Placement
. Вызов функции происходит при срабатывании обработчика события mousemove
.
Как я писал выше, инициализация переноса начнётся, если выполнены два условия: нажата левая кнопка мыши и создан объект, в котором хранится информация о первоначальном положении корабля.
Если условия выполнены, проверяем, существует ли объект clone
и если нет, то создадим его.
Игра «Морской бой». Создание клона перемещаемого корабля.
Рассмотрим подробно часть функции onMouseMove
, отвечающую за создание клона:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// получаем координаты сторон клона корабля let { left, right, top, bottom } = getCoordinates(this.dragObject.el); // если клона ещё не существует, создаём его if (!this.clone) { // получаем количество палуб у перемещаемого корабля this.decks = Placement.getCloneDecks(this.dragObject.el); // создаём клон, используя ранее полученные координаты его сторон this.clone = this.creatClone({left, right, top, bottom}) || null; // если по каким-то причинам клон создать не удалось, выходим из функции if (!this.clone) return; // вычисляем сдвиг курсора по координатам X и Y this.shiftX = this.dragObject.downX - left; this.shiftY = this.dragObject.downY - top; // z-index нужен для позиционирования клона над всеми элементами DOM this.clone.style.zIndex = '1000'; // перемещаем клон в BODY document.body.appendChild(this.clone); } |
Как мы видим, при создании клона используются две функции: getCoordinates
и creatClone
. Функцию getCoordinates
мы рассмотрели в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.», а вот функцию creatClone
мы рассмотрим сейчас подробно.
Предварительно вспомним, что из себя представляет объект dragObject
, который был создан при нажатии на левую кнопку мыши. Это очень важный объект, играющий огромную роль при перетаскивании:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
this.dragObject = { el, // перетаскиваемый корабль parent: el.parentElement, next: el.nextElementSibling, // координаты, с которых начат перенос downX: e.pageX, downY: e.pageY, // координаты 'left' и 'top' используются при редактировании // положения корабля на игровом поле left: el.offsetLeft, top: el.offsetTop, // горизонтальное положение корабля kx: 0, ky: 1 }; |
Итак, сначала функция creatClone
сохраняет в свойстве clone
перетаскиваемый элемент. Затем в свойстве oldPosition
сохраняет сам объект dragObject
, тем самым запоминаются начальные координаты перетаскиваемого корабля, его родительский элемент, а так же элемент следующий за ним.
Следует сделать уточнение относительно родительского элемента корабля. Если мы перетаскиваем корабль на игровое поле, то родительским элементом является контейнер, из которого мы корабли перетаскиваем. В случае редактирования положения корабля (редактирование будет рассмотрено ниже), родительским элементом является само игровое поле.
Также функция creatClone
содержит метод объекта clone
— rollback
. Данный метод позволяет вернуть перетаскиваемый корабль в исходную позицию, если при отпускании кнопки мыши, его координаты были невалидны. Координаты исходной позиции берутся из ранее созданного объекта oldPosition
.
Полный код функции creatClone
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
creatClone() { const clone = this.dragObject.el; const oldPosition = this.dragObject; clone.rollback = () => { // редактирование положения корабля // получаем родительский элемент и // возвращаем корабль на исходное место на игровом поле if (oldPosition.parent == humanfield) { clone.style.left = `${oldPosition.left}px`; clone.style.top = `${oldPosition.top}px`; clone.style.zIndex = ''; oldPosition.parent.insertBefore(clone, oldPosition.next); this.createShipAfterMoving(); } else { // возвращаем корабль в контейнер 'shipsCollection' clone.removeAttribute('style'); oldPosition.parent.insertBefore(clone, oldPosition.next); } }; return clone; } |
Функцию createShipAfterMoving
разберём подробно, когда будем рассматривать окончание переноса. На этом инициирование перемещения закончено — клон корабля создан. Давайте посмотрим, как выглядит функция onMouseMove
на данном этапе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
onMouseMove(e) { if (!this.pressed || !this.dragObject.el) return; // получаем координаты сторон клона корабля let { left, right, top, bottom } = getCoordinates(this.dragObject.el); // если клона ещё не существует, создаём его if (!this.clone) { // получаем количество палуб у перемещаемого корабля this.decks = Placement.getCloneDecks(this.dragObject.el); // создаём клон, используя ранее полученные координаты его сторон this.clone = this.creatClone({left, right, top, bottom}) || null; // если по каким-то причинам клон создать не удалось, выходим из функции if (!this.clone) return; // вычисляем сдвиг курсора по координатам X и Y this.shiftX = this.dragObject.downX - left; this.shiftY = this.dragObject.downY - top; // z-index нужен для позиционирования клона над всеми элементами DOM this.clone.style.zIndex = '1000'; // перемещаем клон в BODY document.body.appendChild(this.clone); } // здесь будет код, отображающий перенос элемента и // валидацию его координат } |
Зададим созданному новые координаты, которые будут соответствовать координатам курсора, с учётом его смещения относительно левого-верхнего угла клона.
Добавим следующий код в конец функции onMouseMove
:
1 2 3 4 5 6 7 8 |
// координаты клона относительно BODY с учётом сдвига курсора // относительно верней левой точки let currentLeft = Math.round(e.pageX - this.shiftX), currentTop = Math.round(e.pageY - this.shiftY); this.clone.style.left = `{currentLeft}px`; this.clone.style.top = `${currentTop}px`; |
Игра «Морской бой». Валидация координат корабля при перетаскивании.
Теперь необходима валидация новых координат, т. е. нужно проверить, что клон полностью находится над игровым полем игрока. Для этого достаточно сравнить координаты его сторон с координатами соответствующих сторон игрового поля. Координаты сторон клона были получены при запуске функции onMouseMove
, а координаты сторон игрового поля являются свойствами объекта FRAME_COORDS_COORDS
. Посмотрим, как будет выглядеть сравнение сторон:
1 2 3 |
if (left >= Placement.FRAME_COORDS_COORDS.left && right <= Placement.FRAME_COORDS_COORDS.right && top >= Placement.FRAME_COORDS_COORDS.top && bottom <= Placement.FRAME_COORDS_COORDS.bottom) { ... } |
Согласитесь, позиционировать объект с точностью до пикселя достаточно затруднительно. Давайте разрешим небольшую погрешность позиционирования в размере 14px. В этом случае, если одна или две стороны клона будет выступать за пределы игрового поля не более чем на 14px, ошибкой считаться не будет. При отпускании кнопки мыши, эта погрешность будет устранена, а клон втянется в пределы игрового поля. Как это реализовано рассмотрим позже.
Теперь условие сравнения сторон будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if (left >= Placement.FRAME_COORDS_COORDS.left - 14 && right <= Placement.FRAME_COORDS_COORDS.right + 14 && top >= Placement.FRAME_COORDS_COORDS.top - 14 && bottom <= Placement.FRAME_COORDS_COORDS.bottom + 14) { // клон находится в пределах игрового поля, // подсвечиваем его контур зелёным цветом this.clone.classList.remove('unsuccess'); this.clone.classList.add('success'); } else { // клон находится за пределами игрового поля, // подсвечиваем его контур красным цветом this.clone.classList.remove('success'); this.clone.classList.add('unsuccess'); } |
Далее необходимо проверить, что в соседних с клоном клетках нет ранее установленных кораблей. Для этого преобразуем абсолютные координаты клона в координаты матрицы связанные с сеткой игрового поля.
Создадим новую функцию getCoordsCloneInMatrix
, аргументом которой будут абсолютные координаты сторон клона. Эта функция является методом класса Placement
.
Рассмотрим по шагам алгоритм преобразования координат:
Полный код функции getCoordsCloneInMatrix
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
getCoordsCloneInMatrix({ left, right, top, bottom } = coords) { // вычисляем разницу координат соотвествующих сторон // клона и игрового поля let computedLeft = left - Placement.FRAME_COORDS_COORDS.left, computedRight = right - Placement.FRAME_COORDS_COORDS.left, computedTop = top - Placement.FRAME_COORDS_COORDS.top, computedBottom = bottom - Placement.FRAME_COORDS_COORDS.top; // создаём объект, куда поместим итоговые значения const obj = {}; // в результате выполнения условия, убираем неточности позиционирования клона let ft = (computedTop < 0) ? 0 : (computedBottom > Field.FIELD_SIDE) ? Field.FIELD_SIDE - Field.SHIP_SIDE : computedTop; let fl = (computedLeft < 0) ? 0 : (computedRight > Field.FIELD_SIDE) ? Field.FIELD_SIDE - Field.SHIP_SIDE * this.decks : computedLeft; obj.top = Math.round(ft / Field.SHIP_SIDE) * Field.SHIP_SIDE; obj.left = Math.round(fl / Field.SHIP_SIDE) * Field.SHIP_SIDE; // переводим значение в координатах матрицы obj.x = obj.top / Field.SHIP_SIDE; obj.y = obj.left / Field.SHIP_SIDE; return obj; } |
Зная координату первой палубы клона и количество его палуб, можно проверить, нет ли в соседних клетках ранее установленных кораблей. Для этого будем использовать функцию checkLocationShip
.
Более подробно о функции
checkLocationShip
изложено в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»
По результатам работы данной функции определяем, валидны или нет координаты, по которым мы собираемся установить корабль.
Теперь полный код проверки валидности координат будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
if (left >= Placement.FRAME_COORDS_COORDS.left - 14 && right <= Placement.FRAME_COORDS_COORDS.right + 14 && top >= Placement.FRAME_COORDS_COORDS.top - 14 && bottom <= Placement.FRAME_COORDS_COORDS.bottom + 14) { // клон находится в пределах игрового поля, // подсвечиваем его контур зелёным цветом this.clone.classList.remove('unsuccess'); this.clone.classList.add('success'); const { x, y } = this.getCoordsCloneInMatrix({ left, right, top, bottom }); const obj = { x, y, kx: this.dragObject.kx, ky: this.dragObject.ky }; const result = human.checkLocationShip(obj, this.decks); if (!result) { // в соседних клетках находятся ранее установленные корабли, // подсвечиваем его контур красным цветом this.clone.classList.remove('success'); this.clone.classList.add('unsuccess'); } } else { // клон находится за пределами игрового поля, // подсвечиваем его контур красным цветом this.clone.classList.remove('success'); this.clone.classList.add('unsuccess'); } |
Добавим этот код код в конец функции onMouseMove
.
На этом визуальное перемещение корабля закончено.
Игра «Морской бой». Окончание переноса корабля на игровое поле.
Событие окончания переноса инициируется при отпускании левой кнопки мыши, запуская обработчик события onmouseup
. Функционал обработки данного события реализован в функции onMouseUp
, являющейся методом класса Placement
.
1 2 3 4 |
document.addEventListener('mouseup', this.onMouseUp.bind(this)); onMouseUp = function(e) { ... } |
Функция onMouseUp
выполняет следующие ключевые задачи:
Настало время написать JavaScript, реализующий данный алгоритм.
Первое, что делает функция onMouseUp
— проверяет существование объекта clone
и, если данного объекта не существует (возможно был сделан клик вне переносимого элемента), то обработчик события onmouseup
прекращает свою работу.
Следующим шагом функция onMouseUp
проверяет валидные или нет координаты клона в момент отпускания кнопки мыши. Нет необходимости проводить полную валидацию координат в том объёме, как это делалось в функции onMouseMove
. Достаточно проверить класс, присвоенный клону:
— success
— координаты валидны, можно продолжить работу обработчика событий;
— unsuccess
— координаты невалидны.
Если окажется, что корабль пытаются поставить в запретные координаты, то возвращаем его в место, откуда было начато перемещение. Для этого будем использовать ранее созданную нами функцию rollback
.
В случае валидных координат, создаём экземпляр нового корабля, исходя из координат клона на момент отпускания кнопки мышки.
После этого удаляем объекты clone
и dragObject
, используя для этого функцию removeClone
. Данная функция очень простая и состоит всего из двух строк:
1 2 3 4 5 6 |
removeClone() { delete this.clone; this.dragObject = {}; } |
Посмотрим, как теперь выглядит полный код функции onMouseUp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
onMouseUp(e) { this.pressed = false; // если клона не существует if (!this.clone) return; // если координаты клона невалидны, возвращаем его на место, // откуда был начат перенос if (this.clone.classList.contains('unsuccess')) { this.clone.classList.remove('unsuccess'); this.clone.rollback(); } else { // создаём экземпляр нового корабля, исходя // из окончательных координат клона this.createShipAfterMoving(); } // удаляем объекты 'clone' и 'dragObject' this.removeClone(); } |
Рассмотрим новую функцию createShipAfterMoving
, являющуюся методом класса Placement
и создающую экземпляр нового корабля, на основе его клона. Задачи, решаемые данной функцией:
JS-код функции createShipAfterMoving
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
createShipAfterMoving() { // получаем координаты, пересчитанные относительно игрового поля const coords = getCoordinates(this.clone); let { left, top, x, y } = this.getCoordsCloneInMatrix(coords); this.clone.style.left = `${left}px`; this.clone.style.top = `${top}px`; // переносим клон внутрь игрового поля humanfield.appendChild(this.clone); this.clone.classList.remove('success'); // создаём объект со свойствами нового корабля const options = { shipname: Placement.getShipName(this.clone), x, y, kx: this.dragObject.kx, ky: this.dragObject.ky, decks: this.decks }; // создаём экземпляр нового корабля const ship = new Ships(human, options); ship.createShip(); // теперь в игровом поле находится сам корабль, поэтому его клон удаляем из DOM humanfield.removeChild(this.clone); } |
Более подробно о создании экземпляра корабля и конструкторе
Ships
написано в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»
Итак, мы рассмотрели расстановку кораблей игрока с помощью метода Drag’n’Drop. В следующей статье будет рассказано, как с помощью JavaScript реализовать:
— редактирование местоположения корабля;
— изменение направления корабля путём поворота на 90°.