Игра Морской бой на JavaScript. Редактирование положения корабля и начало игры.
Вступление.
Третья статья из цикла «Игра Морской бой на чистом JavaScript». В этой статье мы рассмотрим:
- Редактирование положение кораблей на игровом поле.
- Изменения направления расположения палуб путём поворота корабля на 90°.
- Начало игры.
- Инициализацию контроллера игры.
Прежде чем читать дальше, необходимо ознакомиться со статьёй «Игра Морской бой на JavaScript. Расстановка кораблей методом перетаскивания.», иначе вам не всё будет понятно в этой статье.
Игра «Морской бой». Редактирование положения корабля.
Код редактирования положения корабля на игровом поле практически ничем не отличается от кода, реализующего его перемещения. При этом используются те же самые обработчики событий onMouseDown
, onMouseMove
и onMouseUp
с небольшими изменениями и дополнениями.
Игра «Морской бой». Начало редактирования положения корабля.
Инициирование редактирования положения корабля так же, как и начало переноса, начинается с нажатия левой кнопки мыши на корабль. Отличие состоит в том, что в объект draggable
, в котором запоминаются свойства переносимого элемента, записывается дополнительная информация — значения left
и top
, прописанные в атрибуте style
. Эта информация будет использоваться для возвращения корабля в исходное место при невалидных координатах его нового местоположения.
Корабль может быть расположен как горизонтально, так и вертикально. Поэтому необходимо получить направление расположения его палуб. Для этого будем использовать функцию getDirectionShip
. Кроме этого, данная функция записывает полученный результат в объект draggable
в виде значений коэффициентов kx
и ky
. Аргументом функции является имя корабля.
Как вы помните, полная информация о каждом экземпляре корабля хранится в массиве squadron
и, зная имя корабля, эту информацию можно получить из этого массива.
Код функции getDirectionShip
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Instance.prototype.getDirectionShip = function(shipname) { var data; // обходим массив с данными кораблей игрока for (var i = 0, length = user.squadron.length; i < length; i++) { // записываем в переменную информацию по текущему кораблю data = user.squadron[i]; // если имя текущего корабля массива и редактируемого совпадают, то // записываем значения kx и ky в объект draggable if (data.shipname === shipname) { this.draggable.kx = data.kx; this.draggable.ky = data.ky; return; } } } |
Обновлённый код обработчика события onmousedown
:
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 |
Instance.prototype.onMouseDown = function(e) { // если нажатие не на левую кнопку или игра запущена // прекращаем работу функции if (e.which != 1 || userfield.startGame) return; // ищем корабль, ближайший к координатам нажатия на кнопку var el = e.target.closest('.ship'); // если корабль не найден, прекращаем работу функции if (!el) return ; // выставляем флаг нажатия на левую кнопку мышки this.pressed = true; // запоминаем переносимый объект и его свойства this.draggable = { elem: el, //запоминаем координаты, с которых начат перенос downX: e.pageX, downY: e.pageY, kx: 0, ky: 1 }; // нажатие мыши произошло по установленному кораблю, находящемуся // в игровом поле юзера (редактирование положения корабля) if (el.parentElement.getAttribute('id') == 'field_user') { // получаем имя корабля и вызываем функцию, определяющую направление // его положения var name = el.getAttribute('id'); this.getDirectionShip(name); // получаем значения смещения корабля относительно игрового поля и // записываем эти значения в объект draggable // используя метод slice, убираем единицы измерения (px) смещения var computedStyle = getComputedStyle(el); this.draggable.left = computedStyle.left.slice(0, -2); this.draggable.top = computedStyle.top.slice(0, -2); // удаляем экземпляр корабля this.cleanShip(el); } return false; } |
Игра «Морской бой». Визуальное перемещение при редактировании положения корабля.
Перемещение корабля при редактировании его положения ничем не отличается от его первоначальной установки, поэтому используется тот же самый функционал без каких-либо изменений и дополнений.
Игра «Морской бой». Окончание редактирования положения корабля.
За окончание редактирования положения корабля, также, как и за окончание переноса, отвечает функция onMouseUp
, вызываемая обработчиком события onmouseup
.
Редактируемый корабль имеет атрибут style
в котором прописано смещение относительно левого верхнего угла игрового поля. Этот атрибут нужно учитывать при окончании переноса, если придётся возвращать корабль на исходную позицию. Если проигнорировать этот атрибут, то корабль установится в левый верхний угол игрового поля, независимо от первоначального местоположения.
Теперь полный код функции onMouseUp
будет выглядеть так:
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 |
Instance.prototype.onMouseUp = function(e) { // сбрасываем флаг нажатия на левую кнопку мыши this.pressed = false; // если перетаскиваемого объекта не существует, выходим из обработчика событий if (!this.clone) return; // попытка поставить корабль вне игрового поля или в нарушении правил if (this.clone.classList.contains('unsuccess')) { // удаляем класс подсвечивающий контур корабля красным цветом this.clone.classList.remove('unsuccess'); // возвращаем корабль в исходную позицию из которой было начато перемещение this.clone.rollback(); // проверяем наличие значений атрибута style, если значения существуют, то // в данный момент происходит редактирование положения корабля if (this.draggable.left !== undefined && this.draggable.top !== undefined) { // возвращаем корабль на позицию определённую значениями 'left' и 'top', // которые были сохранены в объекте 'draggable' this.draggable.elem.style.cssText = 'left:' + this.draggable.left + 'px; top:' + this.draggable.top + 'px;'; } } else { // получаем координаты привязанные в сетке поля и в координатах матрицы var coords = this.getCoordsClone(this.decks); // переносим клон внутрь игрового поля user.field.appendChild(this.clone); // прописываем координаты клона относительно игрового поля this.clone.style.left = coords.left + 'px'; this.clone.style.top = coords.top + 'px'; // создаём объект со свойствами корабля var fc = { 'shipname': this.clone.getAttribute('id'), 'x': coords.x, 'y': coords.y, 'kx': this.draggable.kx, 'ky': this.draggable.ky, 'decks': this.decks }, // создаём экземпляр корабля ship = new Ships(user, fc); ship.createShip(); // удаляем z-index, т.к. нет необходимости, чтобы корабль был // поверх ВСЕХ элементов getElement(ship.shipname).style.zIndex = null; // теперь в игровом поле находится сам корабль, поэтому его клон удаляем getElement('field_user').removeChild(this.clone); } // удаляем объекты 'clone' и 'draggable' this.cleanClone(); return false; } |
Игра «Морской бой». Поворот корабля на 90°.
Поворот корабля на 90° будем осуществлять кликом по нему правой кнопкой мыши, при котором возникает событие contextmenu
. Чтобы не вешать обработчик события на каждый корабль, воспользуемся делегированием и с помощью метода addEventListener
вешаем всего лишь один обработчик на родительский элемент — fieldUser
. Данный обработчик при срабатывании будет вызывать функцию rotationShip
.
В код, где у нас регистрируются обработчики события, добавим ещё одну строчку:
1 2 3 4 5 6 7 8 9 10 11 |
// нажатие на левую кнопку мышки initialShips.addEventListener('mousedown', this.onMouseDown.bind(this)); fieldUser.addEventListener('mousedown', this.onMouseDown.bind(this)); // перемещение мышки с нажатой кнопкой document.addEventListener('mousemove', this.onMouseMove.bind(this)); // отпускание левой кнопки мышки document.addEventListener('mouseup', this.onMouseUp.bind(this)); // нажатие на правую кнопку мыши fieldUser.addEventListener('contextmenu', this.rotationShip.bind(this)); |
Рассмотрим алгоритм работы функции rotationShip
по шагам:
-
1
Т. к. для поворота корабля используется клик по правой кнопки мыши, необходимо отменить действие браузера по умолчанию — запретить появление контекстного меню. Кроме этого, проверяется состояние флага
startGame
, информирующего о запуске игры. Если флаг установлен, прекращаем работу функции, запрещая этим редактировать положение кораблей. -
2
Получаем
id
корабля, по которому был сделан клик, т. к.id
совпадает с именем корабля. -
3
Полная информация о каждом корабле хранится в массиве
user.squadron
. Обходим данный массив в поисках корабля с именем равным полученномуid
. -
4
Меняем у найденного корабля значения специальных коэффициентов
kx
иky
на противоположные. Еслиkx == 0
иky == 1
— корабль расположен горизонтально, еслиkx == 1
иky == 0
, то вертикально. -
5
При помощи функции
checkLocationShip
проверяем валидность координат при новом направлении расположения палуб.Перед проверкой необходимо удалить экземпляр корабля, иначе результатом работы функции
checkLocationShip
всегда будетfalse
— будет считаться, что в соседних клетках есть какой-то корабль.Если координаты всё же окажутся невалидны, то возвращаем старые значения коэффициентов
kx
иky
. -
6
Используя конструктор
Ships
, создаём новый экземпляр корабля. -
7
Подсветим на 0.5 сек. красным цветом контур корабля, если в результате проверки валидности было получено
false
— так мы визуально покажем, что поворот корабля невозможен.
Более подробно о массиве
squadron
, функции checkLocationShip
, создании экземпляра корабля и конструкторе Ships
изложено в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»
Теперь, когда разобрались с алгоритмом поворота корабля на 90°, напишем функционал, этот алгоритм реализующий. Поскольку прототипом функции rotationShip
является конструктор Instance
, то вызывать её мы будем через Instance.prototype
. Полный код функции:
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 60 61 62 63 64 65 66 67 68 |
Instance.prototype.rotationShip = function(e) { // проверяем, что нажата именно правая кнопка мыши // если установлен флаг начала игры, прекращаем работу // функции if (e.which != 3 || userfield.startGame) { // запрещаем появление контекстного меню при выходе из функции e.preventDefault(); return; } // отменяем действие браузера по умолчанию e.preventDefault(); e.stopPropagation(); // получаем id корабля var id = e.target.getAttribute('id'); // ищем корабль, у которого имя совпадает с полученным id for (var i = 0, length = user.squadron.length; i < length; i++) { // в переменной data сохраняем всю информацию по кораблю из текущей итерации var data = user.squadron[i]; // сравниваем имя корабля с полученным id // у корабля должно быть больше одной палубы - нет смысла вращать однопалубник if (data.shipname == id && data.decks != 1) { // меняем значение коэффициэнтов на противоположные var kx = (data.kx == 0) ? 1 : 0, ky = (data.ky == 0) ? 1 : 0; // удаляем экземпляр корабля this.cleanShip(e.target); user.field.removeChild(e.target); // проверяем валидность координат var result = user.checkLocationShip(data.x0, data.y0, kx, ky, data.decks); if (result === false) { // если новые координаты валидацию не прошли, возвращаем коэффициэнтам // предыдущие значения var kx = (kx == 0) ? 1 : 0, ky = (ky == 0) ? 1 : 0; } // создаём экземпляр корабля var fc = { 'shipname': data.shipname, 'x': data.x0, 'y': data.y0, 'kx': kx, 'ky': ky, 'decks': data.decks }, ship = new Ships(user, fc); ship.createShip(); // подсвечиваем рамку корабля красным цветом, присваивая ему на 0.5 сек. // класс 'unsuccess' if (result === false) { var el = getElement(ship.shipname); el.classList.add('unsuccess'); setTimeout(function() { el.classList.remove('unsuccess'); }, 500); } } } return false; } |
Игра «Морской бой». Запуск игры.
Игра запускается при нажатии на кнопку «Play». Изначально она не отображается, пока все десять кораблей эскадры игрока не будут расставлены на игровом поле. Соответственно, эта кнопка видна и при редактировании положения кораблей эскадры.
При клике на кнопку «Play» происходит следующее:
- Убирается поле с инструкцией.
- Выводится игровое поле компьютера и на нём рандомно расставляются корабли. Естественно, визуально они не отображаются.
- Скрывается кнопка «Play» и выводится сообщение, информирующее о начале игры.
-
Удаляются обработчики событий привязанные к игровому полю игрока:
— редактирование положения кораблей;
— поворот корабля на 90°. - Запускается модуль (контроллер) игры.
JavaScript, реализующий данный функционал, не сложный. Особое внимание хочется обратить на расстановку кораблей противника (компьютера). Для этой цели используются та же самая функция, что и при рандомной расстановке кораблей игрока — randomLocationShips
, только перед этим создаётся экземпляр comp
при помощи конструктора Field
.
1 2 3 4 |
comp = new Field(compfield); comp.randomLocationShips(); |
Более подробно о конструкторе
Field
и работе функции randomLocationShips
изложено в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»
Полный код обработчика события запуска игры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
getElement('play').addEventListener('click', function(e) { // скрываем блок инструкции и выбора способа расстановки кораблей getElement('instruction').setAttribute('data-hidden', true); // показываем поле компьютера, создаём объект поля компьютера и расставляем корабли document.querySelector('.field-comp').setAttribute('data-hidden', false); comp = new Field(compfield); comp.randomLocationShips(); // скрываем кнопку запуска игры getElement('play').setAttribute('data-hidden', true); // выводим сообщение над игровыми полями getElement('text_top').innerHTML = 'Морской бой между эскадрами'; // устанавливаем флаг начала игры для запрета редактирования положения кораблей userfield.startGame = true; // Запуск инициализации модуля игры Controller.battle.init(); }); |
Игра «Морской бой». Контроллер управления игрой «Морской бой».
Весь gameplay игры «Морской бой», реализован в модуле Controller
. В данном случае применена наиболее распространённая реализация модуля с использованием самовызывающейся функции, которая скрывает внутренние свойства и методы.
1 2 3 4 5 |
var Controller = (function() { ... })(); |
Внутри модуля Controller
есть объект battle
, в котором и реализован весь функционал игры «Морской бой». В этот объект с помощью простой литеральной нотации записаны все функции определяющие алгоритм игры.
В данной статье мы рассмотрим только инициализацию игры, которая происходит при нажатии на кнопку Play
. Так как первоначально мы реализуем выстрел игрока, то и JS-код инициализации пока будем писать исходя из этого. Конечно, в дальнейшем этот код будет дополнен.
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 |
var Controller = (function() { // объявляем переменные var player, enemy, self, coords, text, srvText = getElement('text_btm'), tm = 0; // литеральный объект var battle = { // инициализация игры init: function() { self = this; // рандомно определяем кто будет стрелять первым: человек или компьютер var rnd = getRandom(1); player = (rnd == 0) ? user : comp; // определяем, кто будет противником, т.е. чей выстрел следующий enemy = (player === user) ? comp : user; // первым стреляет человек if (player === user) { // устанавливаем на игровое поле компьютера обработчики событий // регистрируем обработчик выстрела compfield.addEventListener('click', self.shoot); // регистрируем обработчик визуальной отметки клеток, в которых // однозначно не может быть кораблей противника compfield.addEventListener('contextmenu', self.setEmptyCell); // выводим сообщение о том, что первый выстрел за пользователем self.showServiseText('Вы стреляете первым.'); } }, // вывод сообщений в ходе игры showServiseText: function(text) { // очищаем контейнер от старого сообщения srvText.innerHTML = ''; // выводим новое сообщение srvText.innerHTML = text; } }; // делаем доступ к инициализации публичным, т.е. доступным снаружи модуля return ({ battle: battle, init: battle.init }); })(); |
У нас появилась новая функция showServiseText
, которая является методом объекта battle
. Её назначение — выводить текст в нижней служебной строке, под игровыми полями.
На данном этапе подготовка к игре закончена полностью. В следующей статье мы рассмотрим выстрел игрока.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
-
В данный момент пишу статью. Надеюсь, через неделю будет готова.
Хотел узнать, когда сможете выложить следующую четвертую статью «Морской бой»? Очень интересный ход работы над игрой.