В прошлую пятницу , в нашей серии игр, я начал исследовать архитектуру SeaBattle, сосредоточившись на SeaBattle
init(width, height)
объекта SeaBattle
init(width, height)
а также на связанных вспомогательных функциях rnd(limit)
и supports_html5_storage()
. Эта статья, часть третья из пяти, продолжает исследовать архитектуру игры, фокусируясь на функции update()
и конструкторе makeShip(x, y, bound1, bound2)
.
Обновление SeaBattle
В листинге 1 представлена реализация функции update()
.
update: function() { if (SeaBattle.state == SeaBattle.STATE_INIT) return; if ((SeaBattle.state == SeaBattle.STATE_TITLE || SeaBattle.state == SeaBattle.STATE_WINLOSE || SeaBattle.state == SeaBattle.STATE_RESTART) && keydown.return) { if (SeaBattle.state == SeaBattle.STATE_RESTART) { SeaBattle.score = 0; SeaBattle.lives = 4; } SeaBattle.ship = new SeaBattle.makeShip(SeaBattle.width/2, SeaBattle.height/3, 0, SeaBattle.width-1); SeaBattle.sub = new SeaBattle.makeSub(SeaBattle.rnd(2) == 0 ? -50+SeaBattle.rnd(30) : SeaBattle.width+SeaBattle.rnd(100), 2*SeaBattle.height/3- SeaBattle.rnd(SeaBattle.height/6), -100, SeaBattle.width+100); SeaBattle.state = SeaBattle.STATE_PLAY; } if (SeaBattle.state != SeaBattle.STATE_PLAY) return; if (SeaBattle.explosion != null) { if (SeaBattle.explosion.isShip) SeaBattle.sub.move(); for (var i = 0; i < SeaBattle.MAX_DC; i++) if (SeaBattle.dc[i] != null) if (!SeaBattle.dc[i].move()) SeaBattle.dc[i] = null; for (var i = 0; i < SeaBattle.MAX_TORP; i++) if (SeaBattle.torp[i] != null) if (!SeaBattle.torp[i].move()) SeaBattle.torp[i] = null; if (!SeaBattle.explosion.advance()) { SeaBattle.ship = null; SeaBattle.sub = null; for (var i = 0; i < SeaBattle.MAX_DC; i++) SeaBattle.dc[i] = null; for (var i = 0; i < SeaBattle.MAX_TORP; i++) SeaBattle.torp[i] = null; SeaBattle.state = SeaBattle.STATE_WINLOSE; if (SeaBattle.explosion.isShip) { SeaBattle.lives--; if (SeaBattle.lives == 0) { SeaBattle.state = SeaBattle.STATE_RESTART; SeaBattle.msg = "Game Over! Press RETURN to play "+"again!"; } } else { SeaBattle.score += 100; if (SeaBattle.score > SeaBattle.hiScore) { SeaBattle.hiScore = SeaBattle.score; if (SeaBattle.supports_html5_storage()) localStorage.setItem("hiScore", SeaBattle.hiScore); } } SeaBattle.explosion = null; } return; } if (keydown.left) SeaBattle.ship.moveLeft(); if (keydown.right) SeaBattle.ship.moveRight(); if (keydown.space) { for (var i = 0; i < SeaBattle.MAX_DC; i++) if (SeaBattle.dc[i] == null) { var bound = SeaBattle.hillTops[SeaBattle.ship.x]; SeaBattle.dc[i] = new SeaBattle.makeDepthCharge(bound); SeaBattle.dc[i].setLocation(SeaBattle.ship.x, SeaBattle.ship.y); break; } keydown.space = false; } SeaBattle.sub.move(); if (SeaBattle.sub.x > 0 && SeaBattle.sub.x < SeaBattle.width && SeaBattle.rnd(15) == 1) for (var i = 0; i < SeaBattle.MAX_TORP; i++) if (SeaBattle.torp[i] == null) { SeaBattle.torp[i] = new SeaBattle.makeTorpedo(SeaBattle.height/3); SeaBattle.torp[i].setLocation(SeaBattle.sub.x, SeaBattle.sub.y-SeaBattle.imgTorpedo.height); break; } for (var i = 0; i < SeaBattle.MAX_DC; i++) if (SeaBattle.dc[i] != null) if (!SeaBattle.dc[i].move()) SeaBattle.dc[i] = null; else { if (SeaBattle.intersects(SeaBattle.dc[i].getBBox(), SeaBattle.sub.getBBox())) { SeaBattle.explosion = new SeaBattle.makeExplosion(false); SeaBattle.explosion.setLocation(SeaBattle.dc[i].x, SeaBattle.dc[i].y); SeaBattle.msg = "You win! Press RETURN to keep playing!"; SeaBattle.dc[i] = null; return; } } for (var i = 0; i < SeaBattle.MAX_TORP; i++) if (SeaBattle.torp[i] != null) if (!SeaBattle.torp[i].move()) SeaBattle.torp[i] = null; else { if (SeaBattle.intersects(SeaBattle.torp[i].getBBox(), SeaBattle.ship.getBBox())) { SeaBattle.explosion = new SeaBattle.makeExplosion(true); SeaBattle.explosion.setLocation(SeaBattle.torp[i].x, SeaBattle.torp[i].y); SeaBattle.msg = "You lose! Press RETURN to keep playing!"; SeaBattle.torp[i] = null; return; } } }
Листинг 1: SeaBattle не обновляет игру в состоянии инициализации.
В листинге 1 сначала проверяется свойство state
чтобы узнать, равно ли оно STATE_INIT
. Если это так, функция update()
возвращается. Нет смысла выполнять update()
дальше, пока игровые ресурсы все еще загружаются.
Затем state
сравнивается с STATE_TITLE
, STATE_WINLOSE
и STATE_RESTART
. Игра не находится в игре, когда в этом состоянии. Чтобы запустить его, пользователю необходимо нажать клавишу Return ( keydown.return
существует и имеет значение true).
Если игра перезапускается ( state
равно STATE_RESTART
), счет сбрасывается на ноль, а количество жизней корабля устанавливается на четыре. Независимо от перезапуска, выигрыша / проигрыша или состояния титула, ship
и sub
объекты создаются, и STATE_PLAY
назначается для state
.
Конструктор makeShip(x, y, bound1, bound2)
вызывается для создания корабля. Этот объект горизонтально центрирован и вертикально расположен на одну треть высоты холста ниже верхней части холста. Границы препятствуют перемещению корабля за пределы холста.
Аналогичный конструктор создает подводную лодку. Этот объект горизонтально и случайно расположен за левым или правым краем холста. Это также вертикально и случайно расположено в средней трети холста. Границы выбираются так, чтобы подводная лодка могла выходить за пределы холста.
На этом этапе state
сравнивается с STATE_PLAY
чтобы определить, находится ли SeaBattle в состоянии игры. Предыдущее сравнение с STATE_TITLE
, STATE_WINLOSE
и STATE_RESTART
возможно, провалилось из-за того, что keydown.return
оценивается как false.
Возможность взрыва корабля или подводной лодки должна быть проверена перед тем, как листинг 1 сможет перейти к проверке пользовательского ввода, управляющего кораблем. Нет смысла перемещать или запускать глубинные заряды от взрывающегося корабля, а также перемещать или запускать торпеды от взрывающейся подводной лодки.
Когда происходит взрыв, свойство explosion
ссылается на объект взрыва. Свойство isShip
объекта устанавливается в значение true, когда корабль взрывается. В этом случае подводная лодка все еще может двигаться; эта задача решается путем вызова функции move()
подобъекта.
Любые глубинные заряды или торпеды, которые находились в игре до того, как корабль или подводная лодка начали взрываться, перемещаются путем вызова каждой из функций move()
их объекта. Когда глубинный заряд или торпеда больше не могут двигаться, move()
возвращает false и объект обнуляется.
Функция advance()
объекта explosion
возвращает true, чтобы указать, что взрыв продвигается. Когда он возвращает false, взрыв заканчивается, соответствующие игровые объекты обнуляются, и state
устанавливается в STATE_WINLOSE
.
Если корабль взорвался, количество жизней уменьшается. Когда это значение достигает нуля, игра заканчивается и готовится подходящее сообщение. Однако, если подводная лодка взорвалась, счет увеличивается на 100 баллов, а высокий балл изменяется и сохраняется (при необходимости).
В отсутствие взрыва следующей задачей Листинга 1 является проверка нажатий клавиш «стрелка влево», «стрелка вправо» или «пробел». moveLeft()
клавиши со стрелкой влево или вправо приводит к вызову функции moveLeft()
или moveRight()
.
Напротив, нажатие пробела приводит к попытке запустить заряд глубины, но только если максимальное количество зарядов глубины не находится в игре. Начальное местоположение глубинного заряда — центр корабля, а его нижняя граница — вершина холма, совпадающая с координатой x корабля.
Подводная лодка теперь перемещается, запускает торпеду, если она не находится за пределами экрана, произвольно выбранное целое число равно определенному значению, а максимальное количество торпед не задействовано. Начальное местоположение торпеды — центр подводной лодки, меньшая высота торпеды, а ее верхняя граница — водная линия.
Наконец, в листинге 1 проверяется столкновение между глубинным зарядом и подводной лодкой или между торпедой и кораблем. Столкновение приводит к созданию объекта взрыва с местоположением, установленным в координаты заряда / торпеды глубины, и подходящему сообщению, назначаемому msg
.
Создание корабля
Функция update()
отвечает за создание корабля-разрушителя и других игровых объектов. Он выполняет создание корабля с помощью конструктора makeShip(x, y, bound1, bound2)
. В листинге 2 представлена реализация этого конструктора.
makeShip: function(x, y, bound1, bound2) { this.x = x; this.y = y; this.bound1 = bound1; this.bound2 = bound2; this.bbox = { left: 0, top: 0, right: 0, bottom: 0 }; this.LEFT = 0; this.RIGHT = 1; this.dir = this.LEFT, this.exploded = false; this.height = SeaBattle.imgShipLeft.height; this.vx = 2; this.width = SeaBattle.imgShipLeft.width; this.draw = function() { SeaBattle.ctx.drawImage((this.dir == this.LEFT)? SeaBattle.imgShipLeft : SeaBattle.imgShipRight, this.x-this.width/2, this.y-this.height/2); return; } this.getBBox = function() { this.bbox.left = this.x-this.width/2; this.bbox.top = this.y-this.height/2; this.bbox.right = this.x+this.width/2; this.bbox.bottom = this.y+2; return this.bbox; } this.moveLeft = function() { this.dir = this.LEFT; this.x -= this.vx; if (this.x-this.width/2 < this.bound1) { this.x += this.vx; this.vx = SeaBattle.rnd(4)+1; } } this.moveRight = function() { this.dir = this.RIGHT; this.x += this.vx; if (this.x+this.width/2 > this.bound2) { this.x -= this.vx; this.vx = SeaBattle.rnd(4)+1; } } }
Листинг 2: Нижняя часть ограничивающей рамки корабля поднята так, что торпеда взрывается ближе к днищу корабля.
Листинг 2 сначала сохраняет свои аргументы в одноименных свойствах объекта корабля, а затем вводит еще 12 свойств объекта:
-
bbox
ссылается на прямоугольный объект, который служит ограничительной рамкой для обнаружения столкновений. Этот объект передается в качестве аргумента функцииintersects(r1, r2)
. -
LEFT
— псевдо-константа, используемая вместе со свойствомdir
. -
RIGHT
— это псевдо-константа, используемая вместе со свойствомdir
. -
dir
указывает текущее направление корабля (влево или вправо). Корабль изначально смотрит налево. -
exploded
указывает,exploded
ли корабль (если ему присвоено значение «истина») или нет (если ему присвоено значение «ложь») -
height
определяет высоту изображения корабля в пикселях. -
vx
определяет горизонтальную скорость корабля с точки зрения количества пикселей, которые корабль движет. Значение по умолчанию равно двум. -
width
определяет ширину изображения корабля в пикселях. -
draw()
рисует изображение корабля влево или вправо. Изображение рисуется так, чтобы его центральное расположение совпадало со значениями свойствx
иy
объекта корабля. -
getBBox()
возвращает обновленный объектbbox
. Этот объект обновляется с учетом изменения горизонтального положения корабля. -
moveLeft()
перемещает корабль влево на количество пикселей, указанное вvx
. Когда корабль достигает левого края холста, он не может двигаться дальше влево, и его скорость изменяется. -
moveRight()
перемещает корабль вправо на количество пикселей, указанное вvx
. Когда корабль достигает правого края холста, он не может двигаться дальше вправо, и его скорость меняется.
Вывод
Функция update()
использует makeShip(x, y, bound1, bound2)
вместе с другими конструкторами с префиксом make
для создания различных игровых объектов. Четвертая часть этой серии игр продолжает исследовать архитектуру SeaBattle, сосредоточив внимание на этих других конструкторах, а также на функции intersects(r1, r2)
, которая позволяет обнаруживать столкновения. Увидимся в следующую пятницу!